Структуры и методы в Go: значение, которое копируется целиком
Собеседование по структурам идёт лестницей: нужен ли структуре конструктор — что будет после b := a — можно ли сравнить две структуры и сделать структуру ключом карты — сколько она весит — на каком типе можно объявить метод. Прогон отвечает на каждый шаг: после b := a правка в поле-срезе видна в a, а в поле-массиве — нет; == на структуре со срезом не собирается, а за интерфейсом — паникует; struct{ a bool; b int64; c bool } на linux/amd64 весит 24 байта, а с int64 первым — 16; и в замере на go1.24.7 множество map[string]struct{} на миллион ключей весит столько же, сколько map[string]bool, — 56 МБ.
Полное техническое изложение
TL;DR
Структура — составной тип-значение: последовательность именованных полей,
которые лежат в памяти подряд. Классов, конструкторов и наследования в Go
нет: есть структура, функции и методы, объявленные на типе. Значение
структуры без инициализации — все поля нулевые, и хорошо написанный тип
работает прямо с таким значением: var mu sync.Mutex уже можно брать, var buf bytes.Buffer — писать.
Главное следствие: присваивание и передача копируют структуру целиком — но
только её собственные байты. После b := a правка b не видна в a для
строк, чисел и массивов таких значений, а для срезов, карт и указателей видна:
копия поверхностная, копируются заголовки (у среза это указатель на массив,
длина и запас), а не данные за ними. slices.Clone и maps.Clone тоже
копируют только один уровень. range по срезу
структур тоже отдаёт копию, и v.X = 0 в цикле элемент не меняет.
Дальше — то, что отличает понимание от пересказа. == для структуры
разрешён, только если сравнимы все поля: структура со срезом не сравнивается
и не может быть ключом карты, а сравнение её за интерфейсом собирается и
паникует; struct{ X any } сравним при компиляции, но паникует, если в X
лежит срез. Размер — не сумма полей: на linux/amd64 struct{ a bool; b int64; c bool } весит 24 байта, та же структура с int64 первым — 16, компилятор
поля не переставляет. Пустая структура весит 0 байт, но в конце другой структуры
добавляет к ней байт, и выравнивание его раздувает: за int64 — до восьми.
Поэтому в нашем замере (go1.24.7, linux/amd64) множество map[string]struct{}
на миллион ключей весит столько же, сколько map[string]bool: 56 МБ. А метод — функция
с получателем первым аргументом, и объявить её можно только на типе своего
пакета.
- что такое переменная, тип и функция в Go;
- что срез и карта хранят данные не в самой переменной: у среза есть заголовок (указатель на массив, длина, запас), у карты — указатель на таблицу; подробно — в уроках о срезах и о картах;
- что такое указатель:
&xи*p; подробно — в следующем уроке, об указателях; - для Механизма 3 — что значение типа
anyхранит внутри значение любого типа; подробно — в уроке об интерфейсах.
- нулевое значение структуры и когда оно рабочее;
- поверхностная копия, сравнение
==, структура ключом карты, размер, выравнивание и пустая структура; - где можно объявить метод и почему метод — это функция.
Что здесь на самом деле спрашивают
Лестница почти всегда такая:
- «Что такое структура? Есть ли в Go классы и конструкторы?» — разминка;
проверяют, знает ли кандидат, что вместо конструктора есть нулевое
значение и обычная функция
New…. - «Что будет, если присвоить структуру другой переменной и поменять поле?» — здесь проверяют, понимает ли кандидат, что структура — значение, и где кончается копия.
- «Можно ли сравнить две структуры? Сделать структуру ключом карты?» — про сравнимость полей.
- «Сколько весит структура? Почему важен порядок полей? Зачем
struct{}?» — про выравнивание. - «На каком типе можно объявить метод? Чем метод отличается от функции?» — про то, что метод привязан к типу своего пакета.
Выбор получателя — значение или указатель — следующий шаг этой лестницы, и ему отдан отдельный урок; встраиванию структур — ещё один.
Стержень у лестницы один: структура — это значение, её поля лежат подряд в памяти, и копирование, сравнение и размер работают с этими байтами целиком — но не с тем, на что они указывают.
База: структура — значение из полей
Спецификация определяет структуру без единого слова о классах:
A struct is a sequence of named elements, called fields, each of which has a
name and a type.
Структура — последовательность именованных элементов, называемых полями, у каждого из которых есть имя и тип.
Объявление, литерал и обращение к полю — фрагмент, полный файл ниже:
type Point struct{ X, Y int }
p := Point{X: 1, Y: 2} // литерал с именами полей
var z Point // нулевое значение: {0 0}
pp := &p // указатель на структуру
pp.X = 7 // то же, что (*pp).X = 7Последняя строка — удобство языка: через указатель на структуру к полю обращаются той же точкой, без явного разыменования.
Вывод bench/gostruct/internals.go, раздел 2 (функция moveRight и срез pts
объявлены в том же файле):
2) ПРИСВАИВАНИЕ И ПЕРЕДАЧА КОПИРУЮТ СТРУКТУРУ
─────────────────────────────────────────────
q := p; q.X = 10 -> p.X = 1, q.X = 10
moveRight(p) прибавил 100 к копии -> p.X = 1
for _, v := range pts { v.X = 0 } -> pts[0].X = 1
for i := range pts { pts[i].X = 0 } -> pts[0].X = 0
pp := &p; pp.X = 7 -> p.X = 7 (через указатель — тот же p)
Этого достаточно для ответа уровня Junior на вопрос «что такое структура»:
структура — составной тип из именованных полей. Это значение, а не ссылка:
q := p создаёт независимую копию, и функция получает копию. Конструкторов в
языке нет — нулевое значение структуры состоит из нулевых значений полей, а
функции NewPoint пишут по соглашению, когда нулевого значения мало.
Всё это помещается в короткую программу. Полный файл
cases/example/main.go из каталога bench/gostruct можно скопировать в пустой каталог рядом
с go.mod (module example, go 1.24) и запустить командой go run .:
package main
import "fmt"
type Point struct{ X, Y int }
func (p Point) Add(q Point) Point { return Point{X: p.X + q.X, Y: p.Y + q.Y} }
type Team struct {
Name string
Members []string
}
func main() {
var z Point
fmt.Println(z)
p := Point{X: 1, Y: 2}
q := p
q.X = 10
fmt.Println(p, q, p == Point{X: 1, Y: 2})
fmt.Println(p.Add(q))
seen := map[Point]bool{p: true}
fmt.Println(seen[Point{X: 1, Y: 2}])
a := Team{Name: "core", Members: []string{"ann"}}
b := a
b.Members[0] = "bob"
fmt.Println(a.Members[0])
}Вывод bench/gostruct/example.sh:
go1.24.7 linux/amd64 | полный пример: go run .
{0 0}
{1 2} {10 2} true
{11 4}
true
bob
Нулевая Point — {0 0}; копия q не трогает p; структуры сравниваются
== и работают ключом карты; метод Add вызывается через точку. Последняя
строка — первая неожиданность урока: b — копия a, но элемент среза
поменялся у обоих. Почему, разобрано в Механизме 2.
Одну таблицу стоит запомнить сразу — она отвечает на два вопроса из лестницы; откуда каждая клетка, разобрано в Механизмах 2 и 3:
| поле | после b := a и правки в b | == для структуры |
|---|---|---|
число, строка, bool | у a своё значение | можно |
массив значений, например [3]int | у a свой массив | можно, если сравним тип элемента |
массив указателей [N]*T | массив свой, но указатели в нём те же — объекты общие | можно — сравниваются адреса |
| указатель | общее значение под ним | можно — сравниваются адреса |
интерфейс, например any | копируется значение интерфейса; срез или указатель внутри остаются общими | собирается, но паника, если внутри несравнимое значение |
| срез, карта | общие данные | нельзя |
Строка «срез, карта» — источник почти всех сюрпризов урока: копия есть, а
независимости нет, и сравнить нельзя вовсе. Строка «интерфейс» — источник
второго: компилятор пропускает ==, а паника приходит во время выполнения.
Механизм 1: нулевое значение — рабочее, если тип так написан
В Go нет конструкторов. Всё, что объявлено без инициализации, получает нулевое значение, и для структуры спецификация делает это поле за полем:
This initialization is done recursively, so for instance each element of an
array of structs will have its fields zeroed if no value is specified.
Эта инициализация рекурсивна: например, у каждого элемента массива структур поля будут обнулены, если значение не задано.
Вопрос в том, можно ли с таким значением работать. Четыре типа, ни у одного
не вызван конструктор. Вывод bench/gostruct/internals.go, раздел 1:
1) НУЛЕВОЕ ЗНАЧЕНИЕ БЕЗ КОНСТРУКТОРА
────────────────────────────────────
var mu sync.Mutex; mu.Lock() -> mu.TryLock(): false (замок уже взят)
var buf bytes.Buffer; WriteString -> "ok", Len(): 2
var s Stats; s.Add("go") -> s.Count("go"): 1
var r Registry; r.Add("go") -> паника: assignment to entry in nil map
нулевой Bag: {Name: Arr:[0 0 0] Slice:[] Map:map[] Ptr:<nil>}
Ни у одного из четырёх типов нет вызова конструктора. Mutex, Buffer
и Stats написаны так, что нулевое значение рабочее; Registry — нет.
sync.Mutex и bytes.Buffer работают, потому что так обещает их
документация: The zero value for a Mutex is an unlocked mutex.
(Нулевое значение Mutex — незаблокированный мьютекс.)
и The zero value for Buffer is an empty buffer ready to use.
(Нулевое значение Buffer — пустой буфер, готовый к работе.)
Свои два типа различаются одной проверкой — фрагмент того же файла:
type Registry struct {
names map[string]int
}
func (r *Registry) Add(name string) { r.names[name]++ }
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]++
}Нулевое значение поля-карты — nil, а в такую карту, по спецификации, можно
читать, но нельзя добавлять: A nil map is equivalent to an empty map except that no elements may be added.
(Карта nil равносильна пустой карте, за исключением того, что в неё нельзя добавлять элементы.)
Registry молча рассчитывает, что карту создаст конструктор, и падает на
первой записи. Stats проверяет поле и создаёт карту сам — ему конструктор
не нужен.
Отсюда ответ на «а как же NewBuffer?». Функции New… — соглашение, а не
часть языка: это обычные функции, возвращающие значение или указатель. В
пакете bytes есть NewBuffer, но и var buf bytes.Buffer работает; в
пакете sync функции NewMutex нет вовсе. Конструктор нужен там, где у типа
есть обязательная настройка — адрес сервера, зависимость, — без которой
нулевое значение не имеет смысла. Во всех остальных случаях хороший тип
пишут так, чтобы var x T уже было рабочим.
Механизм 2: копия целиком — но поверхностная
Блок из Базы показал первую половину: q := p даёт независимую копию,
функция moveRight прибавила 100 к своей копии, а range по срезу структур
отдал копию элемента. Это одно правило, а не три. Передача аргумента — то же
присваивание: the arguments of the call are passed to the function, which means that they are assigned to their corresponding function parameters
(аргументы вызова передаются функции, то есть присваиваются соответствующим параметрам).
А range assigns iteration values to corresponding iteration variables
(присваивает значения итерации соответствующим переменным итерации).
Поэтому v.X = 0 в цикле меняет копию, а элемент меняют через индекс:
pts[i].X = 0.
Вторая половина — что именно копируется. Структура Bag с полями пяти
видов; b := a, затем в b меняется всё. Фрагмент bench/gostruct/internals.go:
type Bag struct {
Name string
Arr [3]int
Slice []int
Map map[string]int
Ptr *int
}Вывод того же файла, раздел 3:
3) КОПИЯ ПОВЕРХНОСТНАЯ
──────────────────────
b := a, затем в b меняем всё:
string a.Name = "a" — не изменилось
[3]int a.Arr[0] = 1 — не изменилось
[]int a.Slice[0] = 99 — изменилось
map a.Map["k"] = 99 — изменилось
*int *a.Ptr = 99 — изменилось
unsafe.Sizeof(a) = 80 байт: копируются заголовки, а не данные за ними
Копируются ровно 80 байт самой структуры. Массив лежит внутри неё — и
копируется. Срез — это заголовок из указателя, длины и запаса; карта и *int —
указатели; копия получает те же адреса, и правка через них видна обоим. Строка
тоже заголовок, но строки неизменяемы: b.Name = "b" не правит общие байты, а
кладёт в поле b другую строку.
Практическое следствие: глубокой копии в языке нет, и стандартные
slices.Clone и maps.Clone её тоже не делают. Документация говорит прямо:
The elements are copied using assignment, so this is a shallow clone
(Элементы копируются присваиванием, поэтому это поверхностная копия)
— и у maps.Clone так же. Clone даёт новый контейнер, а элементы в нём —
копии элементов, то есть для вложенного среза, карты или указателя — те же
адреса. Вывод того же файла, раздел 9:
9) CLONE ТОЖЕ ПОВЕРХНОСТНЫЙ
───────────────────────────
[]int: slices.Clone, c[0] = 99 -> s[0] = 1
[][]int: slices.Clone, c[0][0] = 99 -> s[0][0] = 99
map[string]int: maps.Clone, c["k"] = 99 -> m["k"] = 1
map[string][]int: maps.Clone, c["k"][0] = 99 -> m["k"][0] = 99
[2]*int: b := a, *b[0] = 99 -> *a[0] = 99
Doc: doc.Clone(), c.Grid[0][0] = 99 -> doc.Grid[0][0] = 1
Clone копирует один уровень: контейнер новый, элементы — те же
значения, и если элемент — срез, карта или указатель, данные за
ним общие. Глубину копии решает метод Clone самого типа.
Для поля []int или map[string]int клона контейнера достаточно: элементы —
числа. Для [][]int, map[string][]int и полей-указателей нужно копировать
рекурсивно — столько уровней, сколько требует контракт типа. Массив указателей
[2]*int копируется целиком, но копируются сами указатели, а не объекты за
ними. Поэтому независимую копию пишут методом Clone самого типа и в его
комментарии говорят, что он разрывает, а что оставляет общим. Фрагмент
bench/gostruct/internals.go:
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
}Что делить намеренно — решение автора типа: например, общий *log.Logger или
неизменяемые данные можно оставить общими, а изменяемое состояние — нет.
Один тип копировать нельзя вовсе, и компилятор об этом не скажет.
Документация sync.Mutex пишет прямо: A Mutex must not be copied after first use.
(Mutex нельзя копировать после первого использования.)
Копия структуры с мьютексом — копия замка вместе с его состоянием. Вывод
bench/gostruct/compile.sh, раздел 5 (программа cases/copylock собирается,
а go vet возражает; тексты сообщений — вывод go1.24.7, а не формулировка
языка):
5) КОПИЯ СТРУКТУРЫ С sync.Mutex
───────────────────────────────
go build copylock:
собрано без ошибок
go vet copylock:
./main.go:11:15: report passes lock by value: copylock.Counter contains sync.Mutex
./main.go:16:14: assignment copies lock value to snapshot: copylock.Counter contains sync.Mutex
./main.go:18:13: call of report copies lock value: copylock.Counter contains sync.Mutex
Все три места — обычная копия структуры из этого механизма: параметр-значение,
присваивание и вызов. Ловит их только go vet — и не тот его набор, что
запускает go test: в подмножество проверок, перечисленное в go help test,
copylocks не входит. Поэтому go vet ./... запускают отдельно. Подробнее о
мьютексе — в уроке о sync и atomic.
Цена копии растёт с unsafe.Sizeof, поэтому большие структуры часто передают
указателем. У указателя своя цена — куча, — и обе стороны замерены в уроках
об указателях и о получателе
метода.
Механизм 3: == есть, пока сравнимы все поля
Структуры можно сравнивать оператором == — но не любые:
Struct types are comparable if all their field types are comparable. Two struct
values are equal if their corresponding non-blank field values are equal.
Структурные типы сравнимы, если сравнимы типы всех их полей. Два значения-структуры равны, если равны их соответствующие поля, кроме пустых (_).
Вывод bench/gostruct/internals.go, раздел 4 (Team — структура с полем
Members []string, t1 и t2 — два значения Team в переменных типа any):
4) СРАВНЕНИЕ == И СТРУКТУРА КЛЮЧОМ КАРТЫ
────────────────────────────────────────
Point{1, 2} == Point{1, 2} -> true
grid[Point{1, 2}] -> "treasure"
Node{&x} == Node{&y}, x == y == 5 -> false (указатели сравниваются адресами)
any(Team{}) == any(Team{}) -> паника: runtime error: comparing uncomparable type main.Team
m := map[any]int{}; m[t1] = 1 -> паника: runtime error: hash of unhashable type main.Team
reflect.DeepEqual(t1, t2) -> true
Напрямую a == b для Team не собирается — см. compile.sh, раздел 1.
Первые две строки — главное практическое следствие: структура из сравнимых полей работает ключом карты. Составной ключ — координаты, пара «пользователь, день» — пишут структурой, а не склеивают в строку.
Третья строка — ловушка: Node хранит *int, и == сравнивает указатели,
то есть адреса. Два узла с указателями на равные числа не равны.
Структура Team с полем Members []string несравнима. Компилятор ловит это
в двух местах — вывод bench/gostruct/compile.sh, разделы 1 и 2 (программы в
cases/cmpslice и cases/mapkey намеренно не собираются; текст ошибки —
вывод go1.24.7, а не формулировка спецификации):
1) == НА СТРУКТУРЕ СО СРЕЗОМ
────────────────────────────
go build cmpslice:
./main.go:14:6: invalid operation: a == b (struct containing []string cannot be compared)
2) СТРУКТУРА СО СРЕЗОМ КЛЮЧОМ КАРТЫ
───────────────────────────────────
go build mapkey:
./main.go:13:10: invalid map key type Team
Но за интерфейсом проверка уходит во время выполнения — и это четвёртая
строка прогона, паника. Спецификация оговаривает это отдельно: A comparison of two interface values with identical dynamic types causes a run-time panic if that type is not comparable.
(Сравнение двух значений интерфейса с одинаковым динамическим типом вызывает панику во время выполнения, если этот тип несравним.)
Так бывает в коде, который кладёт значения в map[any]… или сравнивает
any: он собирается, а падает, когда в него впервые придёт структура со
срезом. Для ключа карты сообщение другое — пятая строка прогона, hash of unhashable type: спецификация требует, чтобы для динамических значений
ключа было определено сравнение, иначе — паника. Сравнить такие структуры
можно reflect.DeepEqual — в прогоне он дал true, — но это рефлексия, и в
рабочем коде вместо неё обычно пишут метод Equal с slices.Equal для
полей-срезов.
Та же ловушка прячется внутри структуры. Поле интерфейсного типа
сравнимо, поэтому и вся структура type Key struct{ X any } для компилятора
сравнима: == и ключ карты собираются. Проверка переезжает туда, где лежит
значение. Вывод того же файла, раздел 10:
10) ПОЛЕ-ИНТЕРФЕЙС: СОБИРАЕТСЯ, НО МОЖЕТ ПАНИКОВАТЬ
───────────────────────────────────────────────────
Key{1} == Key{1} -> true
Key{"1"} == Key{1} -> false
Key{[]int{1}} == Key{[]int{1}} -> паника: runtime error: comparing uncomparable type []int
m := map[Key]int{}; m[Key{[]int{1}}] = 1 -> паника: runtime error: hash of unhashable type []int
type Key struct{ X any } сравним: компилятор пропускает и ==, и
ключ карты. Проверка сравнимости значения в X — во время выполнения.
Совет для ключей карт: структура с полем any или другим интерфейсом —
ключ, который собирается всегда, а падает от первого среза. Если ключ
собирают из чужих данных, допустимые динамические типы проверяют до вставки
(или делают поле конкретного сравнимого типа). Тексты паник выше — вывод
go1.24.7 (на go1.25.1 тот же), а не формулировка языка; гарантия
спецификации — сама паника.
Механизм 4: размер — поля плюс выравнивание
Структура занимает не сумму своих полей. Каждое поле лежит по смещению,
кратному его выравниванию, а сама структура выравнена по самому строгому
полю: For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1
(для переменной x структурного типа unsafe.Alignof(x) — наибольшее из значений unsafe.Alignof(x.f) по всем полям f, но не меньше 1).
Сами числа зависят от платформы: размеры и выравнивания ниже — linux/amd64.
Три поля, два порядка; вывод bench/gostruct/internals.go, раздел 5:
5) РАЗМЕР И ВЫРАВНИВАНИЕ: ПОРЯДОК ПОЛЕЙ
───────────────────────────────────────
Alignof: bool 1, int64 8
struct{ a bool; b int64; c bool } Sizeof 24 смещения a=0 b=8 c=16
struct{ b int64; a bool; c bool } Sizeof 16 смещения b=0 a=8 c=9
полезных байт в обоих: 10; выравнивание структуры: 8
В первом порядке b обязан начинаться с адреса, кратного 8, и после a
пропадают семь байт. c занимает байт 16, а ещё семь байт добавляются в
конце: размер структуры кратен её выравниванию, иначе второй элемент массива
таких структур начался бы с невыровненного b. Во втором порядке оба bool
ложатся вплотную за int64, и от 24 байт остаётся 16. Документация
unsafe.Sizeof говорит об этом прямо: For a struct, the size includes any padding introduced by field alignment
(для структуры размер включает все пустые байты, добавленные выравниванием полей).
Компилятор поля не переставляет — порядок задаёт автор типа. Найти лишние
байты помогает анализатор fieldalignment. В обычный go vet он не входит:
его включают в golangci-lint (govet: enable: [fieldalignment]). Вывод
bench/gostruct/compile.sh, раздел 6:
6) ПОРЯДОК ПОЛЕЙ: АНАЛИЗАТОР fieldalignment
───────────────────────────────────────────
golangci-lint has version 2.5.0 built with go1.25.1
golangci-lint run fieldalign:
main.go:4:12: fieldalignment: struct of size 24 could be 16 (govet)
Когда это важно? Когда таких структур миллионы — в срезе, в кеше, в карте — и 24 байта вместо 16 означают в полтора раза больше памяти под те же данные. Для структуры, которая существует в одном экземпляре на запрос, порядок полей лучше выбирать по смыслу: читаемость здесь дороже восьми байт.
Механизм 5: метод — функция, привязанная к типу своего пакета
Метод в Go объявляется отдельно от типа, и привязать его можно не к любому типу:
A receiver base type cannot be a pointer or interface type and it must be
defined in the same package as the method.
Базовый тип получателя не может быть указателем или интерфейсом и должен быть определён в том же пакете, что и метод.
Структурой тип быть не обязан — подойдёт любой определённый тип своего пакета,
кроме указателя и интерфейса. Фрагмент bench/gostruct/internals.go:
type Celsius float64
func (c Celsius) Fahrenheit() float64 { return float64(c)*9/5 + 32 }А на чужом типе — даже на встроенном int — объявить метод нельзя. Вывод
bench/gostruct/compile.sh, раздел 3 (cases/foreignmethod намеренно не
собирается):
3) МЕТОД НА ЧУЖОМ ТИПЕ
──────────────────────
go build foreignmethod:
./main.go:10:9: cannot define new methods on non-local type time.Duration
./main.go:12:9: cannot define new methods on non-local type int
Что метод — это функция, видно по выражению-методу Celsius.Fahrenheit.
Вывод bench/gostruct/internals.go, раздел 7:
7) МЕТОД — ФУНКЦИЯ С ПОЛУЧАТЕЛЕМ
────────────────────────────────
Celsius(100).Fahrenheit() -> 212
Celsius.Fahrenheit(100) -> 212
тип Celsius.Fahrenheit -> func(main.Celsius) float64
time.Duration(1.5 с) -> 1.5s
MyDuration(1.5 с) -> 1500000000 (метода String нет)
MyDuration реализует fmt.Stringer -> false
Тип выражения — func(main.Celsius) float64: получатель стал первым
аргументом. Вызов c.Fahrenheit() — та же функция, которой получатель
передан перед точкой. Отсюда и правило копирования: получатель-значение
получает копию, ровно как параметр из Механизма 2. Метод, который должен
менять поля, объявляют на указателе — так объявлен (*Stats).Add из
Механизма 1. Как выбирать между значением и указателем, разобрано в уроке о
получателе метода.
Последние три строки — ответ на «как добавить метод к чужому типу».
Объявить свой тип на его основе: type MyDuration time.Duration. Но новый тип
получает только базовый тип, не методы — спецификация пишет об определённом
типе: It does not inherit any methods bound to the given type
(он не наследует методы, привязанные к исходному типу).
У MyDuration нет String, и полторы секунды печатаются как
1500000000 наносекунд. Если нужны и старые методы, и новые, чужой тип
кладут полем в свою структуру или встраивают — это урок о
встраивании.
Глубже: пустая структура весит ноль — а множество на ней нет
У пустой структуры нет полей, и весит она ноль. Вывод
bench/gostruct/internals.go, раздел 6 (linux/amd64):
6) ПУСТАЯ СТРУКТУРА
───────────────────
unsafe.Sizeof(struct{}{}) 0
unsafe.Sizeof([1000]struct{}{}) 0
struct{ z struct{}; n int64 } 8 (z по смещению 0)
struct{ n int64; z struct{} } 16 (z по смещению 8)
Пустое поле в конце получает байт, а выравнивание до 8 делает
из него восемь: адрес &s.z не должен указывать за конец s.
Первые две строки — гарантия спецификации: A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero.
(Структура или массив имеют нулевой размер, если в них нет полей (соответственно элементов) с размером больше нуля.)
На этом стоят две идиомы: map[string]struct{} как множество и chan struct{} как сигнал без данных.
Последняя строка — деталь реализации, которую видно только прогоном. Поле
z стоит по смещению 8, сразу за концом n. Будь размер структуры 8, адрес
&s.z указывал бы на следующий объект в куче. Компилятор этого не допускает
(cmd/compile/internal/types/size.go, строки 517–523):
For non-zero-sized structs which end in a zero-sized field, we add an extra
byte of padding to the type. This padding ensures that taking the address of a
zero-sized field can't manufacture a pointer to the next object in the heap.
К ненулевой структуре, которая заканчивается полем нулевого размера, добавляется лишний байт выравнивания. Он гарантирует, что взятие адреса поля нулевого размера не создаст указатель на следующий объект в куче.
Здесь, за int64, выравнивание до 8 превращает байт в восемь. И вот где это выходит наружу. Карта
на go1.24 хранит запись слотом — структурой из ключа и значения, которую
строит компилятор (файл map_swiss.go в каталоге
cmd/compile/internal/reflectdata, строки 30–53). У множества
map[string]struct{} значение в слоте — пустое поле в конце. Вывод
bench/gostruct/sets.go, раздел 2:
2) СЛОТ КАРТЫ: КЛЮЧ И ЗНАЧЕНИЕ ОДНОЙ СТРУКТУРОЙ
───────────────────────────────────────────────
struct{ key string; elem struct{} } 24 байт
struct{ key string; elem bool } 24 байт
struct{ key string; elem int64 } 24 байт
Пустое значение в конце пары получает байт, и выравнивание до 8
делает из пары те же 24 байта, что у bool и int64.
Замер на миллионе строковых ключей подтверждает модель — в этой конфигурации
(go1.24.7, linux/amd64, 2 ядра); на своей машине и версии это надо мерить
заново. Вывод bench/gostruct/sets.go, раздел 1:
1) КАРТА НА 1 000 000 СТРОКОВЫХ КЛЮЧЕЙ
──────────────────────────────────────
map[string]struct{} 56 МБ
map[string]bool 56 МБ
map[string]int64 56 МБ
bool против struct{}: 1.00
Наименьший прирост HeapAlloc из 3 построений; ключи созданы
заранее и в замер не входят.
Все три карты весят одинаково. Совет «множество на struct{} экономит память»
верен для прежней реализации карт, где ключи и значения корзины лежат
раздельно. В go1.24 её ещё можно включить, GOEXPERIMENT=noswissmap, и
разница возвращается. Вывод GOEXPERIMENT=noswissmap go run bench/gostruct/sets.go, раздел 1:
1) КАРТА НА 1 000 000 СТРОКОВЫХ КЛЮЧЕЙ
──────────────────────────────────────
map[string]struct{} 40 МБ
map[string]bool 42 МБ
map[string]int64 58 МБ
bool против struct{}: 1.06
Авторы прежней реализации писали в runtime/map_noswiss.go ровно об этом:
раздельное хранение allows us to eliminate padding which would be needed for, e.g., map[int64]int8
(позволяет избавиться от выравнивания, которое понадобилось бы, например, для map[int64]int8).
Новая реализация по умолчанию включена (internal/buildcfg/exp.go, строка 81:
SwissMap: true) и хранит пару вместе — с выравниванием.
Вывод для собеседования: map[string]struct{} по-прежнему правильная
запись множества — по смыслу: у значения нет смысла, и не бывает записи
m[k] = false, которую легко спутать с отсутствием ключа. Но обещать, что
оно экономит память, нельзя: на go1.24.7 и go1.25.1 замер этого не показал.
Как отвечать на собеседовании
Короткий ответ: структура — составной тип-значение из именованных полей,
которые лежат в памяти подряд. Присваивание и передача копируют её целиком,
но копия поверхностная: срезы, карты и указатели внутри остаются общими, и
slices.Clone с maps.Clone этого не меняют — они копируют один уровень.
Конструкторов нет — нулевое значение структуры состоит из нулевых значений
полей, и хороший тип работает прямо с ним. == разрешён, если сравнимы все
поля; поле-интерфейс компилятор пропускает, а паника приходит при
выполнении.
Методы объявляются на определённом типе своего пакета и по сути — функции с
получателем первым аргументом.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «нужен ли конструктор» — про нулевое значение. «var mu sync.Mutex и
var buf bytes.Buffer работают без инициализации, так записано в их
документации. Свой тип делают таким же: карту создают при первой записи.
New… — соглашение для типов с обязательной настройкой, а не часть языка».
На «что будет после b := a» — где кончается копия. «Копируются байты
самой структуры. Строки, числа и массивы значений у a свои, а через срез,
карту и указатель a и b видят одни данные. Глубокой копии в языке нет, и
slices.Clone с maps.Clone её не делают: они копируют один уровень, у
[][]int внутренние срезы остаются общими. Независимую копию пишут методом
Clone типа, который знает, какие ссылки разорвать. Структуру с
sync.Mutex копировать нельзя, и ловит это go vet, не компилятор».
На «можно ли сравнить» — правило полей и паника за интерфейсом.
«Структура сравнима, если сравнимы все поля, — тогда она и ключ карты.
Со срезом — ошибка компиляции, но за any сравнение собирается и паникует:
comparing uncomparable type. То же с полем any: struct{ X any }
сравним при компиляции и паникует, если в X срез, — поэтому для ключа карты
проверяют, какие типы туда попадут. Указатели сравниваются адресами».
На «сколько весит» — выравнивание. «На linux/amd64 {bool, int64, bool} —
24 байта, с int64 первым — 16: каждое поле выравнивается, размер кратен выравниванию
структуры, компилятор поля не переставляет. Ищет это fieldalignment.
Пустая структура весит ноль, но в конце другой структуры добавляет байт, а
выравнивание может его раздуть — за int64 до восьми; поэтому в моём замере на
go1.24.7 map[string]struct{} оказалась не легче map[string]bool».
На «где объявить метод» — свой пакет. «На любом определённом типе своего
пакета, кроме указателя и интерфейса, не обязательно структуре; на time.Duration или int — ошибка
компиляции. Новый тип на основе чужого методы не наследует. Метод — функция:
Celsius.Fahrenheit имеет тип func(Celsius) float64».
Дальше спросят
Что такое анонимная структура и где она нужна?
Структура без имени типа — объявление и литерал сразу. Вывод
bench/gostruct/internals.go, раздел 8:
8) АНОНИМНАЯ СТРУКТУРА
──────────────────────
cfg := struct{ Host string; Port int }{...} -> {Host:localhost Port:8080}
var pt Point = struct{ X, Y int }{1, 2} -> {X:1 Y:2}
Тип без имени присваивается именованному, если поля совпадают:
имя, тип и тег каждого поля, в том же порядке.
Её берут там, где тип нужен в одном месте: таблица случаев в тесте
([]struct{ name string; in, want int }), разбор одного ответа JSON,
группировка значений внутри функции. Вторая строка показывает, что такая
структура присваивается именованной, если совпадает последовательность полей. Своих
методов у анонимной структуры не бывает: получатель метода должен быть
определённым типом, а у неё нет имени.
Зачем писать имена полей в литерале?
Позиционный литерал geo.Point{1, 2} зависит от порядка и числа полей, и
это видно по трём ответам инструментов — вывод bench/gostruct/compile.sh,
раздел 4 (cases/mixed и cases/unkeyed намеренно не собираются):
4) ЛИТЕРАЛ: ИМЕНА ПОЛЕЙ ИЛИ ПОЗИЦИИ
───────────────────────────────────
go build mixed:
./main.go:7:18: mixture of field:value and value elements in struct literal
go build vetunkeyed (у geo.Point поля X, Y):
собрано без ошибок
go vet vetunkeyed:
./main.go:14:7: vetunkeyed/geo.Point struct literal uses unkeyed fields
go build unkeyed (в geo.Point добавили поле Z):
./main.go:12:21: too few values in struct literal of type geo.Point
Смешивать формы нельзя. Позиционный литерал типа из другого пакета
собирается, но go vet предупреждает — потому что после добавления поля в
следующей версии пакета он перестаёт собираться. Литерал с именами
geo.Point{X: 1, Y: 2} переживает это: новое поле получает нулевое значение.
Для типов своего пакета go vet позиционный литерал пропускает.
Как сравнить две структуры, если внутри срез?
Оператор == не соберётся. Есть два пути. reflect.DeepEqual(a, b) сравнит
поля рекурсивно — в прогоне он дал true для двух Team с одинаковым
содержимым, — но это рефлексия, и по его документации nil-срез и пустой
срез не равны.
В рабочем коде обычно пишут метод — фрагмент-иллюстрация, в прогонах урока
его нет:
func (t Team) Equal(o Team) bool {
return t.Name == o.Name && slices.Equal(t.Members, o.Members)
}Он явно говорит, что считается равенством, и не зависит от рефлексии.
Частые заблуждения
Структура в Go — это класс без наследования
Структура — значение: присваивание создаёт копию, функция получает копию. У неё нет конструктора — есть нулевое значение, — а методы объявляются отдельно и привязаны к типу пакета, а не к структуре: метод можно объявить и на type Celsius float64.
Без функции New… структурой пользоваться нельзя
var mu sync.Mutex и var buf bytes.Buffer работают сразу — так записано в их документации. New… — соглашение для типов с обязательной настройкой. Нулевое значение падает только там, где тип рассчитан на конструктор: assignment to entry in nil map у Registry.
b := a создаёт независимую копию структуры
Независимы только поля, хранящие значение целиком: числа, строки, массивы. Срез, карта и указатель копируются заголовком, и в прогоне правка b.Slice[0], b.Map["k"] и *b.Ptr стала видна в a.
slices.Clone и maps.Clone делают независимую копию
Только на один уровень: документация называет обе shallow clone. В прогоне после slices.Clone у [][]int правка c[0][0] изменила исходный s[0][0], а после maps.Clone у map[string][]int — исходный срез. Для полей []int и map[string]int этого хватает; для вложенных — рекурсивный метод Clone самого типа.
Изменить элемент можно прямо в цикле for _, v := range
v — копия элемента: после v.X = 0 в прогоне pts[0].X осталось 1. Меняют через индекс: pts[i].X = 0.
Если структура не сравнима, это всегда поймает компилятор
Только при прямом сравнении. За интерфейсом any(Team{}) == any(Team{}) собирается и паникует: comparing uncomparable type main.Team.
Раз структура собирается как ключ карты, вставка ключа не упадёт
Не если у неё поле-интерфейс. type Key struct{ X any } сравним при компиляции, а m[Key{[]int{1}}] = 1 в прогоне упало: hash of unhashable type []int.
Компилятор сам расположит поля оптимально
Не переставляет: поля лежат в порядке объявления. Те же три поля на linux/amd64 весят 24 байта при порядке bool, int64, bool и 16 при int64, bool, bool. Подсказывает анализатор fieldalignment.
Множество на пустой структуре легче, чем на bool
В нашем замере на go1.24.7 и go1.25.1 (linux/amd64) — нет: миллион строковых ключей занял 56 МБ и в map[string]struct{}, и в map[string]bool. Слот карты — пара «ключ, значение», и пустое значение в конце пары выравнивается до восьми байт. Разница — 40 против 42 МБ — есть только у прежней реализации карт.
История версий
Прогоны урока — go1.24.7 и go1.25.1, linux/amd64, 2 ядра; правила языка
сверены с их спецификациями и go doc 14.10.2026. Размеры и смещения
относятся к linux/amd64. На эту дату вышли Go 1.26 и Go 1.27 — урок их не
измерял: в окружении замеров их нет, и ни один размер, текст ошибки или вес
карты на них не проверен.
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| go1.24.7, GOEXPERIMENT=noswissmap | Прежняя реализация карт: ключи и значения корзины лежат раздельно. Миллион строковых ключей — 40 МБ у map[string]struct{}, 42 МБ у map[string]bool, 58 МБ у map[string]int64 (runs/sets-noswissmap.txt). | множество на пустой структуре немного легче, чем на bool |
| go1.24.7 | Реализация карт по умолчанию — Swiss-таблица (SwissMap: true в internal/buildcfg/exp.go, строка 81): слот хранит ключ и значение одной структурой. Все три карты — 56 МБ (runs/sets.txt). Размеры структур, правила копирования и сравнения — как в уроке. | экономии памяти от struct{} в карте нет |
| go1.25.1 | Вывод internals.go, compile.sh и example.sh совпал с go1.24.7 кроме строки версии; карты — те же 56 МБ (runs/sets-go1.25.txt, runs/internals-go1.25.txt). | как на go1.24.7 |
| Go 1.26, Go 1.27 | Вышли к 14.10.2026; урок на них не измерял ни размеры, ни вес карт, ни тексты ошибок. Правила копирования, сравнения и объявления методов — часть спецификации; сверены по спецификациям go1.24.7 и go1.25.1. | на своей версии — мерить заново |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
type Point struct{ X, Y int }
type Team struct {
Name string
Members []string
}
type Event struct {
Active bool
ID int64
Urgent bool
}
p := Point{1, 2}
q := p
q.X = 10
fmt.Println(p.X, q.X)
a := Team{Name: "core", Members: []string{"ann"}}
b := a
b.Name = "infra"
b.Members[0] = "bob"
fmt.Println(a.Name, a.Members[0])
fmt.Println(p == Point{1, 2})
fmt.Println(unsafe.Sizeof(Event{}))Практика · оцените
Проверка знаний
Структура содержит поле Members типа срез строк. После b := a выполнили b.Members[0] = `bob`. Что увидит a.Members[0]?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Структура — составной тип-значение: последовательность именованных полей, которые лежат в памяти подряд. Классов, конструкторов и наследования в Go нет: есть структура, функции и методы, объявленные на типе. Значение структуры без инициализации — все поля нулевые, и хорошо написанный тип работает прямо с таким значением:
var mu sync.Mutexуже можно брать,var buf bytes.Buffer— писать. - Главное следствие: присваивание и передача копируют структуру целиком — но только её собственные байты. После
b := aправкаbне видна вaдля строк, чисел и массивов таких значений, а для срезов, карт и указателей видна: копия поверхностная, копируются заголовки (у среза это указатель на массив, длина и запас), а не данные за ними.slices.Cloneиmaps.Cloneтоже копируют только один уровень.rangeпо срезу структур тоже отдаёт копию, иv.X = 0в цикле элемент не меняет. - Дальше — то, что отличает понимание от пересказа.
==для структуры разрешён, только если сравнимы все поля: структура со срезом не сравнивается и не может быть ключом карты, а сравнение её за интерфейсом собирается и паникует;struct{ X any }сравним при компиляции, но паникует, если вXлежит срез. Размер — не сумма полей: на linux/amd64struct{ a bool; b int64; c bool }весит 24 байта, та же структура сint64первым — 16, компилятор поля не переставляет. Пустая структура весит 0 байт, но в конце другой структуры добавляет к ней байт, и выравнивание его раздувает: заint64— до восьми. Поэтому в нашем замере (go1.24.7, linux/amd64) множествоmap[string]struct{}на миллион ключей весит столько же, сколькоmap[string]bool: 56 МБ. А метод — функция с получателем первым аргументом, и объявить её можно только на типе своего пакета.
На самом деле
- Структура — значение: присваивание создаёт копию, функция получает копию. У неё нет конструктора — есть нулевое значение, — а методы объявляются отдельно и привязаны к типу пакета, а не к структуре: метод можно объявить и на
type Celsius float64. var mu sync.Mutexиvar buf bytes.Bufferработают сразу — так записано в их документации.New…— соглашение для типов с обязательной настройкой. Нулевое значение падает только там, где тип рассчитан на конструктор:assignment to entry in nil mapуRegistry.- Независимы только поля, хранящие значение целиком: числа, строки, массивы. Срез, карта и указатель копируются заголовком, и в прогоне правка
b.Slice[0],b.Map["k"]и*b.Ptrстала видна вa. - Только на один уровень: документация называет обе shallow clone. В прогоне после
slices.Cloneу[][]intправкаc[0][0]изменила исходныйs[0][0], а послеmaps.Cloneуmap[string][]int— исходный срез. Для полей[]intиmap[string]intэтого хватает; для вложенных — рекурсивный методCloneсамого типа. v— копия элемента: послеv.X = 0в прогонеpts[0].Xосталось 1. Меняют через индекс:pts[i].X = 0.- Только при прямом сравнении. За интерфейсом
{any(Team{}) == any(Team{})}собирается и паникует:comparing uncomparable type main.Team. - Не если у неё поле-интерфейс.
{type Key struct{ X any }}сравним при компиляции, а{m[Key{[]int{1}}] = 1}в прогоне упало:hash of unhashable type []int. - Не переставляет: поля лежат в порядке объявления. Те же три поля на linux/amd64 весят 24 байта при порядке
bool, int64, boolи 16 приint64, bool, bool. Подсказывает анализаторfieldalignment. - В нашем замере на go1.24.7 и go1.25.1 (linux/amd64) — нет: миллион строковых ключей занял 56 МБ и в
{map[string]struct{}}, и вmap[string]bool. Слот карты — пара «ключ, значение», и пустое значение в конце пары выравнивается до восьми байт. Разница — 40 против 42 МБ — есть только у прежней реализации карт.
По версиям
- go1.24.7, GOEXPERIMENT=noswissmap
- Прежняя реализация карт: ключи и значения корзины лежат раздельно. Миллион строковых ключей — 40 МБ у
{map[string]struct{}}, 42 МБ уmap[string]bool, 58 МБ уmap[string]int64(runs/sets-noswissmap.txt).< - go1.24.7
- Реализация карт по умолчанию — Swiss-таблица (
SwissMap: trueвinternal/buildcfg/exp.go, строка 81): слот хранит ключ и значение одной структурой. Все три карты — 56 МБ (runs/sets.txt). Размеры структур, правила копирования и сравнения — как в уроке.< - go1.25.1
- Вывод
internals.go,compile.shиexample.shсовпал с go1.24.7 кроме строки версии; карты — те же 56 МБ (runs/sets-go1.25.txt,runs/internals-go1.25.txt).< - Go 1.26, Go 1.27
- Вышли к 14.10.2026; урок на них не измерял ни размеры, ни вес карт, ни тексты ошибок. Правила копирования, сравнения и объявления методов — часть спецификации; сверены по спецификациям go1.24.7 и go1.25.1.<
Что разобрано
- Что здесь на самом деле спрашивают
- База: структура — значение из полей
- Механизм 1: нулевое значение — рабочее, если тип так написан
- Механизм 2: копия целиком — но поверхностная
- Механизм 3: `==` есть, пока сравнимы все поля
- Механизм 4: размер — поля плюс выравнивание
- Механизм 5: метод — функция, привязанная к типу своего пакета
- Глубже: пустая структура весит ноль — а множество на ней нет
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверка знаний
Источники и что читать дальше
7 ИСТОЧНИКОВ
- Спецификация Go — Struct types, The zero valueОфициальная документация. Что такое структура: «A struct is a sequence of named elements, called fields, each of which has a name and a type» (Структура — последовательность именованных элементов, называемых полями, у каждого из которых есть имя и тип). Каким получается значение без инициализации: «This initialization is done recursively, so for instance each element of an array of structs will have its fields zeroed if no value is specified» (Эта инициализация рекурсивна: например, у каждого элемента массива структур поля будут обнулены, если значение не задано). И про карту в нулевом значении: «A nil map is equivalent to an empty map except that no elements may be added» (Карта nil равносильна пустой карте, за исключением того, что в неё нельзя добавлять элементы). Сверено по doc/go_spec.html дистрибутивов go1.24.7 и go1.25.1, 14.10.2026.https://go.dev/ref/spec#Struct_types
- Спецификация Go — Comparison operators, Map typesОфициальная документация. Когда структуры сравнимы: «Struct types are comparable if all their field types are comparable. Two struct values are equal if their corresponding non-blank field values are equal» (Структурные типы сравнимы, если сравнимы типы всех их полей. Два значения-структуры равны, если равны их соответствующие поля, кроме пустых (_)). Что бывает за интерфейсом: «A comparison of two interface values with identical dynamic types causes a run-time panic if that type is not comparable» (Сравнение двух значений интерфейса с одинаковым динамическим типом вызывает панику во время выполнения, если этот тип несравним). И требование к ключу карты: «thus the key type must not be a function, map, or slice» (поэтому типом ключа не может быть функция, карта или срез), а для ключа-интерфейса — «If the key type is an interface type, these comparison operators must be defined for the dynamic key values; failure will cause a run-time panic» (Если тип ключа — интерфейс, эти операторы сравнения должны быть определены для динамических значений ключа; иначе — паника во время выполнения). Сверено 14.10.2026.https://go.dev/ref/spec#Comparison_operators
- Спецификация Go — Calls, Method declarations, Type definitionsОфициальная документация. Почему передача в функцию — тоже копия: «the arguments of the call are passed to the function, which means that they are assigned to their corresponding function parameters» (аргументы вызова передаются функции, то есть присваиваются соответствующим параметрам). Где можно объявить метод: «A receiver base type cannot be a pointer or interface type and it must be defined in the same package as the method» (Базовый тип получателя не может быть указателем или интерфейсом и должен быть определён в том же пакете, что и метод). И что получает новый тип: «It does not inherit any methods bound to the given type» (Он не наследует методы, привязанные к исходному типу).https://go.dev/ref/spec#Method_declarations
- Спецификация Go — Size and alignment guarantees; пакет unsafeОфициальная документация. Выравнивание структуры: «For a variable x of struct type: unsafe.Alignof(x) is the largest of all the values unsafe.Alignof(x.f) for each field f of x, but at least 1» (Для переменной x структурного типа unsafe.Alignof(x) — наибольшее из значений unsafe.Alignof(x.f) по всем полям f, но не меньше 1). Пустая структура: «A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero» (Структура или массив имеют нулевой размер, если в них нет полей (соответственно элементов) с размером больше нуля). Из документации unsafe.Sizeof: «For a struct, the size includes any padding introduced by field alignment» (Для структуры размер включает все пустые байты, добавленные выравниванием полей).https://go.dev/ref/spec#Size_and_alignment_guarantees
- Пакет sync — Mutex; пакет bytes — BufferОфициальная документация. Нулевое значение, готовое к работе, записано в документации типов. sync.Mutex: «The zero value for a Mutex is an unlocked mutex» (Нулевое значение Mutex — незаблокированный мьютекс) и тут же «A Mutex must not be copied after first use» (Mutex нельзя копировать после первого использования). bytes.Buffer (https://pkg.go.dev/bytes#Buffer): «The zero value for Buffer is an empty buffer ready to use» (Нулевое значение Buffer — пустой буфер, готовый к работе).https://pkg.go.dev/sync#Mutex
- Пакеты slices и maps — CloneОфициальная документация. Обе функции копируют один уровень. slices.Clone: «The elements are copied using assignment, so this is a shallow clone» (Элементы копируются присваиванием, поэтому это поверхностная копия). maps.Clone (https://pkg.go.dev/maps#Clone): «This is a shallow clone: the new keys and values are set using ordinary assignment» (Это поверхностная копия: новые ключи и значения задаются обычным присваиванием). Сверено по go doc, go1.24.7, 14.10.2026.https://pkg.go.dev/slices#Clone
- cmd/compile/internal/types/size.go и карты на go1.24.7Исходный код Go. Раскладку структуры считает calcStructOffset (строки 164–196): поля идут в порядке объявления, смещение каждого округляется вверх до его выравнивания; итоговый размер округляется до выравнивания структуры (строка 542). Строки 517–523 — про пустое поле в конце: «For non-zero-sized structs which end in a zero-sized field, we add an extra byte of padding to the type» (К ненулевой структуре, которая заканчивается полем нулевого размера, добавляется лишний байт выравнивания). Слот карты — структура из ключа и значения: cmd/compile/internal/reflectdata/map_swiss.go, строки 30–53. Прежняя реализация карт, runtime/map_noswiss.go, строки 154–157, хранит ключи и значения раздельно: «it allows us to eliminate padding which would be needed for, e.g., map[int64]int8» (это позволяет избавиться от выравнивания, которое понадобилось бы, например, для map[int64]int8). Что по умолчанию включена новая: SwissMap: true в internal/buildcfg/exp.go, строка 81.https://github.com/golang/go/blob/go1.24.7/src/cmd/compile/internal/types/size.go