MEASUREMENT
bench/gostruct/sets.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
143 lines//go:build ignore
// Множество на карте: сколько памяти экономит значение struct{}.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. map[string]struct{} советуют как множество, «потому что
// пустая структура весит 0 байт». Само утверждение верно — unsafe.Sizeof
// пустой структуры равен нулю (internals.go, раздел 6). Но карта хранит не
// значение отдельно, а пару ключ–значение, и пара — это тоже структура. Если
// пустое значение стоит в ней последним, компилятор добавляет к паре байт, а
// выравнивание превращает его в целое слово. Сколько от экономии остаётся,
// видно только замером.
//
// ЧТО МЕРИТСЯ. Три карты на один и тот же миллион строковых ключей:
// значения struct{}, bool и int64. Ключи создаются заранее и в замер не
// входят. Мерится прирост HeapAlloc после runtime.GC(): то, что карта держит
// живым. Каждая карта строится трижды, берётся наименьший прирост; округление
// до целого мегабайта убирает шум в сотню килобайт между запусками.
//
// ВТОРОЙ РАЗДЕЛ — модель слота: структура из ключа и значения, такая же, какую
// компилятор строит для слота карты (cmd/compile/internal/reflectdata/
// map_swiss.go, SwissMapGroupType). Её размер печатает unsafe.Sizeof.
//
// ЗАПУСК:
//
// go run bench/gostruct/sets.go
// GOEXPERIMENT=noswissmap go run bench/gostruct/sets.go # прежняя реализация карт
package main
import (
"fmt"
"os"
"runtime"
"strconv"
"strings"
"unsafe"
)
const N = 1_000_000
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func heap() uint64 {
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m.HeapAlloc
}
var (
sinkEmpty map[string]struct{}
sinkBool map[string]bool
sinkInt map[string]int64
)
// grow строит карту и возвращает, сколько байт кучи она держит. Карта
// остаётся в глобальной переменной до замера, затем отпускается.
func grow(keys []string, build func([]string)) uint64 {
best := ^uint64(0)
for round := 0; round < 3; round++ {
before := heap()
build(keys)
if d := heap() - before; d < best {
best = d
}
sinkEmpty, sinkBool, sinkInt = nil, nil, nil
}
return best
}
type slotEmpty struct {
key string
elem struct{}
}
type slotBool struct {
key string
elem bool
}
type slotInt struct {
key string
elem int64
}
func main() {
exp := os.Getenv("GOEXPERIMENT")
if exp == "" {
exp = "по умолчанию"
}
fmt.Printf("%s %s/%s | множество на карте: сколько весит значение (GOEXPERIMENT: %s)\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH, exp)
keys := make([]string, N)
for i := range keys {
keys[i] = "key-" + strconv.Itoa(i)
}
empty := grow(keys, func(ks []string) {
sinkEmpty = make(map[string]struct{})
for _, k := range ks {
sinkEmpty[k] = struct{}{}
}
})
boolean := grow(keys, func(ks []string) {
sinkBool = make(map[string]bool)
for _, k := range ks {
sinkBool[k] = true
}
})
integer := grow(keys, func(ks []string) {
sinkInt = make(map[string]int64)
for _, k := range ks {
sinkInt[k] = 1
}
})
mb := func(b uint64) float64 { return float64(b) / 1e6 }
show("1) КАРТА НА 1 000 000 СТРОКОВЫХ КЛЮЧЕЙ")
fmt.Printf(" map[string]struct{} %3.0f МБ\n", mb(empty))
fmt.Printf(" map[string]bool %3.0f МБ\n", mb(boolean))
fmt.Printf(" map[string]int64 %3.0f МБ\n", mb(integer))
fmt.Printf(" bool против struct{}: %.2f\n", float64(boolean)/float64(empty))
fmt.Println(" Наименьший прирост HeapAlloc из 3 построений; ключи созданы")
fmt.Println(" заранее и в замер не входят.")
show("2) СЛОТ КАРТЫ: КЛЮЧ И ЗНАЧЕНИЕ ОДНОЙ СТРУКТУРОЙ")
fmt.Printf(" struct{ key string; elem struct{} } %d байт\n", unsafe.Sizeof(slotEmpty{}))
fmt.Printf(" struct{ key string; elem bool } %d байт\n", unsafe.Sizeof(slotBool{}))
fmt.Printf(" struct{ key string; elem int64 } %d байт\n", unsafe.Sizeof(slotInt{}))
if strings.Contains(exp, "noswissmap") {
fmt.Println(" Эта реализация карт так пары не хранит: в корзине сначала лежат")
fmt.Println(" все ключи, затем все значения (runtime/map_noswiss.go), и")
fmt.Println(" выравнивать пустое значение не к чему.")
return
}
fmt.Println(" Пустое значение в конце пары получает байт, и выравнивание до 8")
fmt.Println(" делает из пары те же 24 байта, что у bool и int64.")
}