Deep Engineering

ЗАМЕР

bench/gostruct/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/structs
Как запустить
go run bench/gostruct/internals.go
bash bench/gostruct/compile.sh
go run bench/gostruct/sets.go
GOEXPERIMENT=noswissmap go run bench/gostruct/sets.go
/usr/local/go1.25.1/bin/go run bench/gostruct/sets.go
go run bench/gostruct/practice.go
bash bench/gostruct/example.sh

Запись прогона

Замеры для урока «Структуры и методы»

Файл Что делает
internals.go восемь наблюдений без единого замера времени: нулевое значение без конструктора, копия при присваивании, передаче и range, поверхностность копии, сравнение == и структура ключом карты, размер и выравнивание при двух порядках полей, пустая структура, метод как функция с получателем и определённый тип без методов базового, анонимная структура; после аудита 14.10.2026 — раздел 9 (slices.Clone/maps.Clone поверхностны, массив указателей копирует указатели, рекурсивный Clone) и раздел 10 (поле any: сравнимо при компиляции, паника при выполнении)
compile.sh собирает cases/* (или проверяет их go vet и golangci-lint) и печатает ответ инструмента: == и ключ карты у структуры со срезом, метод на чужом типе, смешанный и позиционный литерал, копия структуры с sync.Mutex, анализатор fieldalignment
cases/*/ подопытные программы к compile.sh, у каждой свой go.mod; большинство намеренно не собирается
sets.go память карты на миллион строковых ключей со значениями struct{}, bool и int64; размер слота «ключ–значение»
example.sh, cases/example/ полный короткий пример урока (go run . в чистом каталоге с go.mod)
practice.go источник ответов двух практических задач урока
go run bench/gostruct/internals.go
bash bench/gostruct/compile.sh
go run bench/gostruct/sets.go
GOEXPERIMENT=noswissmap go run bench/gostruct/sets.go
/usr/local/go1.25.1/bin/go run bench/gostruct/sets.go
go run bench/gostruct/practice.go
bash bench/gostruct/example.sh

Записи прогонов — в runs/, go1.24.7 linux/amd64, 2 ядра: internals.txt, compile.txt, sets.txt, sets-noswissmap.txt (та же go1.24.7 с прежней реализацией карт), example.txt, practice.txt; на go1.25.1 — internals-go1.25.txt, compile-go1.25.txt, example-go1.25.txt, sets-go1.25.txt: отличаются от go1.24.7 только строкой версии. Сверено 14.10.2026; Go 1.26 и 1.27 не измерялись. golangci-lint в compile.sh — версии 2.5.0, собранный go1.25.1.

Что получилось

Нулевое значение может быть рабочим — если тип так написан. var mu sync.Mutex сразу берётся Lock, var buf bytes.Buffer сразу пишется. Свой тип Stats, который создаёт карту при первой записи, тоже работает без конструктора, а Registry, который ждёт карту от конструктора, падает: assignment to entry in nil map.

Присваивание копирует структуру целиком, но копия поверхностная. После b := a и правок в b у a не изменились строка и массив [3]int, а изменились элемент среза, значение в карте и значение под указателем: unsafe.Sizeof(a) = 80 — копируются заголовки, а не данные за ними. range по срезу структур отдаёт копию: v.X = 0 не меняет pts[0].X. Копию структуры с sync.Mutex компилятор пропускает, а go vet ловит в трёх местах.

== у структуры — пока сравнимы все поля. Point{1, 2} == Point{1, 2} — true, Point работает ключом карты. Структура с полем []string не сравнивается (struct containing []string cannot be compared) и не может быть ключом (invalid map key type Team), а за интерфейсом сравнение собирается и паникует: comparing uncomparable type main.Team; ключом map[any]int — тоже паника, с другим текстом: hash of unhashable type main.Team. Указатели сравниваются адресами: две структуры с указателями на равные числа не равны.

Порядок полей меняет размер. struct{ a bool; b int64; c bool } — 24 байта, смещения 0, 8, 16; struct{ b int64; a bool; c bool } — 16, смещения 0, 8, 9. Полезных байт в обоих 10. fieldalignment: struct of size 24 could be 16 — в обычный go vet он не входит, в cases/fieldalign/.golangci.yml он включён явно.

Пустая структура весит 0 — кроме как в конце другой структуры. struct{ z struct{}; n int64 } — 8 байт, struct{ n int64; z struct{} } — 16: за пустым полем в конце компилятор добавляет байт (cmd/compile/internal/types/size.go, строки 517–523 в go1.24.7), и выравнивание делает из него восемь.

Поэтому map[string]struct{} на go1.24 весит столько же, сколько map[string]bool. Миллион строковых ключей: 56 МБ, 56 МБ и 56 МБ у int64, кратность bool к struct{} — 1.00. Слот карты — структура «ключ, значение», и struct{ key string; elem struct{} } весит те же 24 байта, что с bool и int64. На go1.25.1 — те же 56 МБ. С прежней реализацией карт (GOEXPERIMENT=noswissmap) — 40, 42 и 58 МБ: там ключи и значения корзины лежат раздельно.

Метод — функция с получателем первым аргументом. Celsius.Fahrenheit(100) даёт то же, что Celsius(100).Fahrenheit(), а тип выражения — func(main.Celsius) float64. Метод на time.Duration и на int объявить нельзя: cannot define new methods on non-local type. Новый тип MyDuration с тем же базовым int64 методов time.Duration не получает: печатается 1500000000, а не 1.5s.

Литерал: имена полей или позиции. Смешивать нельзя (mixture of field:value and value elements). Позиционный литерал чужого типа собирается, но go vet предупреждает (struct literal uses unkeyed fields), а после того как в тип добавили поле, перестаёт собираться: too few values in struct literal of type geo.Point.

Clone тоже поверхностный (раздел 9). После slices.Clone у [][]int правка c[0][0] видна в исходном (s[0][0] = 99), после maps.Clone у map[string][]int — тоже; у []int и map[string]int — нет. b := a для [2]*int копирует указатели: *a[0] = 99. Метод Doc.Clone, клонирующий каждый внутренний срез, даёт независимую копию: doc.Grid[0][0] = 1.

Поле-интерфейс (раздел 10). type Key struct{ X any } сравним при компиляции; Key{[]int{1}} == Key{[]int{1}} — паника comparing uncomparable type []int, вставка ключом карты — hash of unhashable type []int.

Скрипт

336 строк
//go:build ignore

// Структуры и методы в Go: значение, которое копируется целиком.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про структуры говорят «это как класс без
// наследования» — и упускают то, о чём на самом деле спрашивают: структура в
// Go — значение. Она копируется при присваивании и передаче, сравнивается
// оператором ==, если это позволяют поля, занимает в памяти столько, сколько
// велит выравнивание, а её нулевое значение может быть рабочим без всякого
// конструктора. Каждое из этих утверждений проверяется запуском.
//
// ВОСЕМЬ НАБЛЮДЕНИЙ:
//
//  1. Нулевое значение. sync.Mutex и bytes.Buffer работают без конструктора;
//     свой тип — только если так написан.
//  2. Присваивание, передача в функцию и range копируют структуру целиком.
//  3. Копия поверхностная: массив копируется, а срез, карта и указатель
//     после копии ведут на те же данные.
//  4. Сравнение == и структура ключом карты; несравнимое поле за интерфейсом —
//     паника во время выполнения (ошибки компиляции — в compile.sh).
//  5. Размер и выравнивание: порядок полей меняет unsafe.Sizeof.
//  6. Пустая структура весит 0 байт, но в конце другой структуры — нет.
//  7. Метод — функция с получателем; определённый тип не наследует методы.
//  8. Анонимная структура и присваиваемость именованной.
//
// И два раздела, добавленные после внешнего аудита 14.10.2026:
//
//  9. slices.Clone и maps.Clone тоже копируют поверхностно: вложенные срезы
//     и значения-срезы остаются общими; массив указателей копирует указатели.
//     Глубокую копию делает свой метод Clone, который знает устройство типа.
//  10. Поле-интерфейс: структура сравнима при компиляции, а сравнение и
//     вставка ключом карты паникуют, если внутри лежит срез.
//
// ЗАПУСК:
//
//	go run bench/gostruct/internals.go
package main

import (
	"bytes"
	"fmt"
	"maps"
	"reflect"
	"runtime"
	"slices"
	"strings"
	"sync"
	"time"
	"unsafe"
)

func show(title string) {
	fmt.Println()
	fmt.Println(title)
	fmt.Println(strings.Repeat("─", len([]rune(title))))
}

// panicText выполняет f и возвращает текст паники или "".
func panicText(f func()) (msg string) {
	defer func() {
		if r := recover(); r != nil {
			msg = fmt.Sprint(r)
		}
	}()
	f()
	return ""
}

// --------------------------------------------------- 1. нулевое значение

// Registry ждёт, что карту создаст конструктор. Нулевое значение — ловушка.
type Registry struct {
	names map[string]int
}

func (r *Registry) Add(name string) { r.names[name]++ }

// Stats написан так, чтобы нулевое значение было рабочим: карта создаётся
// при первой записи. Конструктор ему не нужен.
type Stats struct {
	counts map[string]int
}

func (s *Stats) Add(name string) {
	if s.counts == nil {
		s.counts = make(map[string]int)
	}
	s.counts[name]++
}

func (s *Stats) Count(name string) int { return s.counts[name] } // чтение из nil-карты законно

// ------------------------------------------------ 2–3. копия при присваивании

type Point struct{ X, Y int }

func moveRight(p Point) { p.X += 100 } // получает копию

type Bag struct {
	Name  string
	Arr   [3]int
	Slice []int
	Map   map[string]int
	Ptr   *int
}

// ------------------------------------------------------------- 4. сравнение

type Team struct {
	Name    string
	Members []string
}

type Node struct{ Val *int }

// ------------------------------------------------- 5–6. размер и выравнивание

type BoolIntBool struct {
	a bool
	b int64
	c bool
}

type IntBoolBool struct {
	b int64
	a bool
	c bool
}

type EmptyFirst struct {
	z struct{}
	n int64
}

type EmptyLast struct {
	n int64
	z struct{}
}

// ------------------------------------------------------- 7. метод — функция

type Celsius float64

func (c Celsius) Fahrenheit() float64 { return float64(c)*9/5 + 32 }

// MyDuration — новый определённый тип с тем же базовым типом int64, что у
// time.Duration. Методы time.Duration (в том числе String) он не получает.
type MyDuration time.Duration

// ------------------------------------------------- 9. Clone тоже поверхностный

// Doc — тип, которому нужна независимая копия. Его Clone документирует, что
// именно разрывается: и внешний срез, и каждый внутренний.
type Doc struct {
	Title string
	Grid  [][]int
}

// Clone возвращает копию, не делящую с d ни одного массива: внешний срез
// клонируется, и каждый внутренний — тоже.
func (d Doc) Clone() Doc {
	c := d
	c.Grid = make([][]int, len(d.Grid))
	for i, row := range d.Grid {
		c.Grid[i] = slices.Clone(row)
	}
	return c
}

// ----------------------------------------- 10. поле-интерфейс и сравнимость

type Key struct{ X any }

func main() {
	fmt.Printf("%s %s/%s | структуры и методы\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)

	// ---------------------------------------------------------------- 1
	show("1) НУЛЕВОЕ ЗНАЧЕНИЕ БЕЗ КОНСТРУКТОРА")
	var mu sync.Mutex
	mu.Lock()
	fmt.Printf("  var mu sync.Mutex; mu.Lock()      -> mu.TryLock(): %v (замок уже взят)\n", mu.TryLock())
	mu.Unlock()
	var buf bytes.Buffer
	buf.WriteString("ok")
	fmt.Printf("  var buf bytes.Buffer; WriteString -> %q, Len(): %d\n", buf.String(), buf.Len())
	var s Stats
	s.Add("go")
	fmt.Printf("  var s Stats; s.Add(\"go\")          -> s.Count(\"go\"): %d\n", s.Count("go"))
	var r Registry
	fmt.Printf("  var r Registry; r.Add(\"go\")       -> паника: %s\n", panicText(func() { r.Add("go") }))
	fmt.Printf("  нулевой Bag: %+v\n", Bag{})
	fmt.Println("  Ни у одного из четырёх типов нет вызова конструктора. Mutex, Buffer")
	fmt.Println("  и Stats написаны так, что нулевое значение рабочее; Registry — нет.")

	// ---------------------------------------------------------------- 2
	show("2) ПРИСВАИВАНИЕ И ПЕРЕДАЧА КОПИРУЮТ СТРУКТУРУ")
	p := Point{X: 1, Y: 2}
	q := p
	q.X = 10
	fmt.Printf("  q := p; q.X = 10                    -> p.X = %d, q.X = %d\n", p.X, q.X)
	moveRight(p)
	fmt.Printf("  moveRight(p) прибавил 100 к копии   -> p.X = %d\n", p.X)
	pts := []Point{{1, 2}, {3, 4}}
	for _, v := range pts {
		v.X = 0
	}
	fmt.Printf("  for _, v := range pts { v.X = 0 }   -> pts[0].X = %d\n", pts[0].X)
	for i := range pts {
		pts[i].X = 0
	}
	fmt.Printf("  for i := range pts { pts[i].X = 0 } -> pts[0].X = %d\n", pts[0].X)
	pp := &p
	pp.X = 7
	fmt.Printf("  pp := &p; pp.X = 7                  -> p.X = %d (через указатель — тот же p)\n", p.X)

	// ---------------------------------------------------------------- 3
	show("3) КОПИЯ ПОВЕРХНОСТНАЯ")
	n := 1
	a := Bag{Name: "a", Arr: [3]int{1, 2, 3}, Slice: []int{1, 2, 3}, Map: map[string]int{"k": 1}, Ptr: &n}
	b := a
	b.Name = "b"
	b.Arr[0] = 99
	b.Slice[0] = 99
	b.Map["k"] = 99
	*b.Ptr = 99
	fmt.Println("  b := a, затем в b меняем всё:")
	fmt.Printf("  string  a.Name     = %q  — не изменилось\n", a.Name)
	fmt.Printf("  [3]int  a.Arr[0]   = %d    — не изменилось\n", a.Arr[0])
	fmt.Printf("  []int   a.Slice[0] = %d   — изменилось\n", a.Slice[0])
	fmt.Printf("  map     a.Map[\"k\"] = %d   — изменилось\n", a.Map["k"])
	fmt.Printf("  *int    *a.Ptr     = %d   — изменилось\n", *a.Ptr)
	fmt.Printf("  unsafe.Sizeof(a)   = %d байт: копируются заголовки, а не данные за ними\n", unsafe.Sizeof(a))

	// ---------------------------------------------------------------- 4
	show("4) СРАВНЕНИЕ == И СТРУКТУРА КЛЮЧОМ КАРТЫ")
	fmt.Printf("  Point{1, 2} == Point{1, 2}        -> %v\n", Point{1, 2} == Point{1, 2})
	grid := map[Point]string{{1, 2}: "treasure"}
	fmt.Printf("  grid[Point{1, 2}]                 -> %q\n", grid[Point{1, 2}])
	x, y := 5, 5
	fmt.Printf("  Node{&x} == Node{&y}, x == y == 5 -> %v (указатели сравниваются адресами)\n", Node{&x} == Node{&y})
	var t1, t2 any = Team{Name: "core"}, Team{Name: "core"}
	fmt.Printf("  any(Team{}) == any(Team{})        -> паника: %s\n", panicText(func() { _ = t1 == t2 }))
	byAny := map[any]int{}
	fmt.Printf("  m := map[any]int{}; m[t1] = 1     -> паника: %s\n", panicText(func() { byAny[t1] = 1 }))
	fmt.Printf("  reflect.DeepEqual(t1, t2)         -> %v\n", reflect.DeepEqual(t1, t2))
	fmt.Println("  Напрямую a == b для Team не собирается — см. compile.sh, раздел 1.")

	// ---------------------------------------------------------------- 5
	show("5) РАЗМЕР И ВЫРАВНИВАНИЕ: ПОРЯДОК ПОЛЕЙ")
	var bib BoolIntBool
	var ibb IntBoolBool
	fmt.Printf("  Alignof: bool %d, int64 %d\n", unsafe.Alignof(bib.a), unsafe.Alignof(bib.b))
	fmt.Printf("  struct{ a bool; b int64; c bool }  Sizeof %2d  смещения a=%d b=%d c=%d\n",
		unsafe.Sizeof(bib), unsafe.Offsetof(bib.a), unsafe.Offsetof(bib.b), unsafe.Offsetof(bib.c))
	fmt.Printf("  struct{ b int64; a bool; c bool }  Sizeof %2d  смещения b=%d a=%d c=%d\n",
		unsafe.Sizeof(ibb), unsafe.Offsetof(ibb.b), unsafe.Offsetof(ibb.a), unsafe.Offsetof(ibb.c))
	fmt.Printf("  полезных байт в обоих: %d; выравнивание структуры: %d\n",
		unsafe.Sizeof(bib.a)+unsafe.Sizeof(bib.b)+unsafe.Sizeof(bib.c), unsafe.Alignof(bib))

	// ---------------------------------------------------------------- 6
	show("6) ПУСТАЯ СТРУКТУРА")
	fmt.Printf("  unsafe.Sizeof(struct{}{})          %2d\n", unsafe.Sizeof(struct{}{}))
	fmt.Printf("  unsafe.Sizeof([1000]struct{}{})    %2d\n", unsafe.Sizeof([1000]struct{}{}))
	var ef EmptyFirst
	var el EmptyLast
	fmt.Printf("  struct{ z struct{}; n int64 }      %2d  (z по смещению %d)\n", unsafe.Sizeof(ef), unsafe.Offsetof(ef.z))
	fmt.Printf("  struct{ n int64; z struct{} }      %2d  (z по смещению %d)\n", unsafe.Sizeof(el), unsafe.Offsetof(el.z))
	fmt.Println("  Пустое поле в конце получает байт, а выравнивание до 8 делает")
	fmt.Println("  из него восемь: адрес &s.z не должен указывать за конец s.")

	// ---------------------------------------------------------------- 7
	show("7) МЕТОД — ФУНКЦИЯ С ПОЛУЧАТЕЛЕМ")
	fmt.Printf("  Celsius(100).Fahrenheit()          -> %v\n", Celsius(100).Fahrenheit())
	fmt.Printf("  Celsius.Fahrenheit(100)            -> %v\n", Celsius.Fahrenheit(100))
	fmt.Printf("  тип Celsius.Fahrenheit             -> %T\n", Celsius.Fahrenheit)
	d := 1500 * time.Millisecond
	fmt.Printf("  time.Duration(1.5 с)               -> %v\n", d)
	fmt.Printf("  MyDuration(1.5 с)                  -> %v (метода String нет)\n", MyDuration(d))
	_, hasString := any(MyDuration(d)).(fmt.Stringer)
	fmt.Printf("  MyDuration реализует fmt.Stringer  -> %v\n", hasString)

	// ---------------------------------------------------------------- 8
	show("8) АНОНИМНАЯ СТРУКТУРА")
	cfg := struct {
		Host string
		Port int
	}{"localhost", 8080}
	fmt.Printf("  cfg := struct{ Host string; Port int }{...} -> %+v\n", cfg)
	var pt Point = struct{ X, Y int }{1, 2}
	fmt.Printf("  var pt Point = struct{ X, Y int }{1, 2}     -> %+v\n", pt)
	fmt.Println("  Тип без имени присваивается именованному, если поля совпадают:")
	fmt.Println("  имя, тип и тег каждого поля, в том же порядке.")

	// ---------------------------------------------------------------- 9
	show("9) CLONE ТОЖЕ ПОВЕРХНОСТНЫЙ")
	flat := []int{1, 2}
	flatC := slices.Clone(flat)
	flatC[0] = 99
	fmt.Printf("  []int:            slices.Clone, c[0] = 99        -> s[0] = %d\n", flat[0])
	nested := [][]int{{1, 2}, {3, 4}}
	nestedC := slices.Clone(nested)
	nestedC[0][0] = 99
	fmt.Printf("  [][]int:          slices.Clone, c[0][0] = 99     -> s[0][0] = %d\n", nested[0][0])
	counts := map[string]int{"k": 1}
	countsC := maps.Clone(counts)
	countsC["k"] = 99
	fmt.Printf("  map[string]int:   maps.Clone, c[\"k\"] = 99        -> m[\"k\"] = %d\n", counts["k"])
	lists := map[string][]int{"k": {1, 2}}
	listsC := maps.Clone(lists)
	listsC["k"][0] = 99
	fmt.Printf("  map[string][]int: maps.Clone, c[\"k\"][0] = 99     -> m[\"k\"][0] = %d\n", lists["k"][0])
	one, two := 1, 2
	ptrs := [2]*int{&one, &two}
	ptrsC := ptrs
	*ptrsC[0] = 99
	fmt.Printf("  [2]*int:          b := a, *b[0] = 99             -> *a[0] = %d\n", *ptrs[0])
	doc := Doc{Title: "plan", Grid: [][]int{{1, 2}, {3, 4}}}
	docC := doc.Clone()
	docC.Grid[0][0] = 99
	fmt.Printf("  Doc:              doc.Clone(), c.Grid[0][0] = 99 -> doc.Grid[0][0] = %d\n", doc.Grid[0][0])
	fmt.Println("  Clone копирует один уровень: контейнер новый, элементы — те же")
	fmt.Println("  значения, и если элемент — срез, карта или указатель, данные за")
	fmt.Println("  ним общие. Глубину копии решает метод Clone самого типа.")

	// ---------------------------------------------------------------- 10
	show("10) ПОЛЕ-ИНТЕРФЕЙС: СОБИРАЕТСЯ, НО МОЖЕТ ПАНИКОВАТЬ")
	fmt.Printf("  Key{1} == Key{1}                   -> %v\n", Key{1} == Key{1})
	fmt.Printf("  Key{\"1\"} == Key{1}                 -> %v\n", Key{"1"} == Key{1})
	k1, k2 := Key{[]int{1}}, Key{[]int{1}}
	fmt.Printf("  Key{[]int{1}} == Key{[]int{1}}     -> паника: %s\n", panicText(func() { _ = k1 == k2 }))
	byKey := map[Key]int{}
	fmt.Printf("  m := map[Key]int{}; m[Key{[]int{1}}] = 1 -> паника: %s\n", panicText(func() { byKey[k1] = 1 }))
	fmt.Println("  type Key struct{ X any } сравним: компилятор пропускает и ==, и")
	fmt.Println("  ключ карты. Проверка сравнимости значения в X — во время выполнения.")
}