Deep Engineering

MEASUREMENT

bench/gostruct/compile.sh

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

84 lines
#!/usr/bin/env bash
#
# Что о структурах говорят компилятор, go vet и анализатор fieldalignment.
#
# ЗАЧЕМ ОТДЕЛЬНЫЙ СКРИПТ. Программы с этими ошибками не собираются, поэтому
# показать их из internals.go нельзя. Здесь собирается (или проверяется
# go vet) каждая из cases/*, и печатается ответ инструмента — без строк
# «# пакет» и без путей к каталогу.
#
# СЛУЧАИ:
#
#  cmpslice      — == на структуре с полем-срезом;
#  mapkey        — та же структура ключом карты;
#  foreignmethod — метод на типе чужого пакета и на встроенном int;
#  mixed         — литерал, где часть полей с именами, часть без;
#  unkeyed       — позиционный литерал чужого типа после того, как в тип
#                  добавили поле;
#  vetunkeyed    — тот же литерал до добавления поля: собирается, но vet
#                  предупреждает;
#  copylock      — копия структуры с sync.Mutex: собирается, vet возражает;
#  fieldalign    — анализатор fieldalignment (golangci-lint) о порядке полей.
#
# У каждого каталога в cases/ свой go.mod: это отдельные модули, и
# `go vet ./...` в bench/gostruct их не трогает.
#
# ЗАПУСК:
#
#	bash bench/gostruct/compile.sh
#	GO=/usr/local/go1.25.1/bin/go bash bench/gostruct/compile.sh
set -u

GO=${GO:-go}
cd "$(dirname "$0")/cases"

echo "$($GO version | awk '{print $3, $4}') | ошибки компилятора, go vet и fieldalignment"

section() {
	echo
	echo "$1"
	printf '─%.0s' $(seq 1 "$(printf '%s' "$1" | LC_ALL=C.UTF-8 wc -m)")
	echo
}

build() { (cd "$1" && $GO build -o /dev/null . 2>&1) | grep -v '^#' | sed 's/^/  /'; }
vet() { (cd "$1" && $GO vet . 2>&1) | grep -v '^#' | sed 's/^/  /'; }

section "1) == НА СТРУКТУРЕ СО СРЕЗОМ"
echo "  go build cmpslice:"
build cmpslice

section "2) СТРУКТУРА СО СРЕЗОМ КЛЮЧОМ КАРТЫ"
echo "  go build mapkey:"
build mapkey

section "3) МЕТОД НА ЧУЖОМ ТИПЕ"
echo "  go build foreignmethod:"
build foreignmethod

section "4) ЛИТЕРАЛ: ИМЕНА ПОЛЕЙ ИЛИ ПОЗИЦИИ"
echo "  go build mixed:"
build mixed
echo "  go build vetunkeyed (у geo.Point поля X, Y):"
if (cd vetunkeyed && $GO build -o /dev/null . 2>/dev/null); then
	echo "  собрано без ошибок"
fi
echo "  go vet vetunkeyed:"
vet vetunkeyed
echo "  go build unkeyed (в geo.Point добавили поле Z):"
build unkeyed

section "5) КОПИЯ СТРУКТУРЫ С sync.Mutex"
echo "  go build copylock:"
if (cd copylock && $GO build -o /dev/null . 2>/dev/null); then
	echo "  собрано без ошибок"
fi
echo "  go vet copylock:"
vet copylock

section "6) ПОРЯДОК ПОЛЕЙ: АНАЛИЗАТОР fieldalignment"
echo "  $(golangci-lint --version | awk '{print $1, $2, $3, $4, $5, $6, $7}')"
echo "  golangci-lint run fieldalign:"
(cd fieldalign && golangci-lint run ./... 2>&1) |
	grep 'fieldalignment' | sed 's/^/  /'