MEASUREMENT
bench/gostruct/internals.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/structs
- How to run it
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
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Структуры и методы»
| Файл | Что делает |
|---|---|
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.
Script
336 lines//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 — во время выполнения.")
}