Структуры и методы
Struct — основной агрегатный тип в Go. Без классов и наследования его дополняет embedding (композиция) и method sets. На собесе джуна спросят: “что такое padding/alignment”, “зачем
struct{}”, “разница value vs pointer receiver”, “что такое method set у T и *T”. Здесь — детально, под капотом.
Содержание
Заголовок раздела «Содержание»- Базовое определение и API
- Внутреннее устройство (ПОД КАПОТОМ)
- Тонкие моменты / Gotchas
- Производительность / Memory considerations
- Типичные вопросы на собеседовании Junior
- Practice — мини-задачки
- Источники
1. Базовое определение и API
Заголовок раздела «1. Базовое определение и API»Объявление struct
Заголовок раздела «Объявление struct»type User struct { ID int64 Name string Email string}
u := User{ID: 1, Name: "Anna", Email: "a@b.c"}u2 := User{1, "Bob", "b@c.d"} // позиционно (плохая практика для >2-3 полей)u3 := User{Name: "Empty"} // остальные — zero valuesAnonymous (literal) struct
Заголовок раздела «Anonymous (literal) struct»pair := struct { X, Y int}{1, 2}Удобно в тестах, для temporary types, иногда в JSON-парсинге.
Доступ к полям и мутация
Заголовок раздела «Доступ к полям и мутация»u.Name = "new"fmt.Println(u.Email)Указатель на struct — . работает прозрачно
Заголовок раздела «Указатель на struct — . работает прозрачно»p := &up.Name = "via ptr" // эквивалент (*p).Name = "via ptr"Go автоматически разыменовывает, поэтому (*p).field обычно не пишут.
Empty struct — struct{}
Заголовок раздела «Empty struct — struct{}»type set map[string]struct{}s := set{}s["foo"] = struct{}{}_, ok := s["foo"]Размер struct{} — 0 байт (unsafe.Sizeof(struct{}{}) == 0). Use cases:
- множества:
map[K]struct{}экономит память относительноmap[K]bool; - сигнальные каналы:
chan struct{}для “беспэйлоадных” нотификаций.
type Counter struct{ n int }
func (c Counter) Get() int { return c.n } // value receiverfunc (c *Counter) Inc() { c.n++ } // pointer receiverИмя получателя — короткое, обычно первая буква типа (c, u, s).
Embedding (композиция)
Заголовок раздела «Embedding (композиция)»type Animal struct{ Name string }func (a Animal) Greet() string { return "Hi, I'm " + a.Name }
type Dog struct { Animal // встроено Breed string}
d := Dog{Animal: Animal{Name: "Rex"}, Breed: "Husky"}fmt.Println(d.Greet()) // "Hi, I'm Rex" — метод "промоутед"fmt.Println(d.Name) // "Rex" — поле тоже промоутедtype User struct { ID int `json:"id" db:"user_id"` Name string `json:"name,omitempty" validate:"required"`}Tags — это просто строки, доступные через reflect. Их трактуют JSON, sqlx, validator, и т.д.
Constructor pattern
Заголовок раздела «Constructor pattern»В Go нет конструкторов как языковой конструкции. Идиома — функция NewX:
func NewUser(name string) *User { return &User{ ID: nextID(), Name: name, CreatedAt: time.Now(), }}2. Внутреннее устройство (ПОД КАПОТОМ)
Заголовок раздела «2. Внутреннее устройство (ПОД КАПОТОМ)»Memory layout
Заголовок раздела «Memory layout»Поля struct лежат в памяти в порядке объявления (Go не переставляет поля). Но между ними может вставляться padding для выравнивания (alignment).
type Bad struct { A byte // 1 байт B int64 // 8 байт (требует выравнивания по 8) C byte // 1 байт}Без выравнивания:
A | B | C1 | 8 | 1 = 10 байт?С учётом alignment (поле B должно начинаться с адреса кратного 8):
[A][pad pad pad pad pad pad pad][B B B B B B B B][C][pad pad pad pad pad pad pad] 1 7 байт padding 8 1 7 байт padding (до 8) = 24 байтаПереставим:
type Good struct { B int64 // 8 A byte // 1 C byte // 1 // 6 байт padding до выравнивания структуры}// = 16 байтКоманды для проверки
Заголовок раздела «Команды для проверки»import "unsafe"
fmt.Println(unsafe.Sizeof(Bad{})) // 24fmt.Println(unsafe.Sizeof(Good{})) // 16fmt.Println(unsafe.Alignof(int64(0))) // 8fmt.Println(unsafe.Offsetof(Bad{}.B)) // 8Утилита fieldalignment от golang.org/x/tools подсказывает оптимальный порядок:
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latestfieldalignment ./...Empty struct — 0 байт, но особый адрес
Заголовок раздела «Empty struct — 0 байт, но особый адрес»var a, b struct{}fmt.Println(&a == &b) // не специфицировано, может быть truefmt.Println(unsafe.Sizeof(a)) // 0Runtime использует один и тот же адрес для всех значений struct{} (zerobase).
Value receiver vs pointer receiver
Заголовок раздела «Value receiver vs pointer receiver»func (c Counter) Get() int { return c.n } // получает копию Counterfunc (c *Counter) Inc() { c.n++ } // получает указательЧто выбрать:
- Pointer receiver: нужно мутировать, struct большая (>64 байт), хотите консистентность.
- Value receiver: маленький immutable тип (time.Time, money), нет мутаций.
⚠️ Правило согласованности: если у типа есть хоть один pointer receiver — все методы делайте pointer receiver. Иначе method set путается.
Method set
Заголовок раздела «Method set»Method set — набор методов, привязанных к типу. Влияет на удовлетворение интерфейсов.
| Тип | Method set |
|---|---|
T | Все методы с receiver T (value) |
*T | Все методы с receiver T и *T |
type Greeter interface{ Greet() }
type Hello struct{}func (h Hello) Greet() {} // value receivervar _ Greeter = Hello{} // OKvar _ Greeter = &Hello{} // OK (через *T)
type Hi struct{}func (h *Hi) Greet() {} // pointer receivervar _ Greeter = &Hi{} // OK// var _ Greeter = Hi{} // ❌ Hi не имеет Greet(), только *HiПочему? Потому что компилятор может автоматически взять адрес addressable-значения для вызова pointer-метода. Но если у вас Hi{} как rvalue (не addressable), адрес взять нельзя.
Под капотом метод = функция со скрытым receiver
Заголовок раздела «Под капотом метод = функция со скрытым receiver»func (c *Counter) Inc() { c.n++ }// Эквивалентно:// Counter_Inc(c *Counter) { c.n++ }c.Inc() под капотом: Counter_Inc(&c).
Method value vs method expression
Заголовок раздела «Method value vs method expression»type T struct{ X int }func (t T) Get() int { return t.X }
t := T{X: 10}mv := t.Get // method value: receiver уже привязан (копия t)fmt.Println(mv()) // 10t.X = 20fmt.Println(mv()) // 10 (потому что внутри mv хранится копия t)
me := T.Get // method expressionfmt.Println(me(t)) // 20 (передаём t явно)Если в method value receiver — указатель, обновления видны:
type C struct{ X int }func (c *C) Get() int { return c.X }
c := &C{X: 1}mv := c.Getc.X = 2fmt.Println(mv()) // 2Embedding под капотом
Заголовок раздела «Embedding под капотом»Embedding — это просто анонимное поле. Промоушн методов — синтаксический сахар:
type Dog struct { Animal Breed string}// d.Greet() == d.Animal.Greet()// d.Name == d.Animal.NameЭто композиция, не наследование. Embedded type не знает о Dog. Нельзя “переопределить” метод так, чтобы вызов из Animal попал в Dog’овский (нет полиморфизма базовый-вызывает-производный).
Embedding pointer vs value
Заголовок раздела «Embedding pointer vs value»type A struct{ X int }type B struct{ A } // value embedtype C struct{ *A } // pointer embed
b := B{A: A{X: 1}}c := C{A: &A{X: 1}}Pointer embed экономит копирование при включении большой структуры; даёт nil как валидное “ничего”.
Tags — это просто string
Заголовок раздела «Tags — это просто string»type T struct { F int `json:"f" db:"f_col"`}json пакет через reflect.StructTag.Get("json") парсит тэги. Format: key:"value" через пробел.
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»⚠️ Gotcha 1: Padding раздувает размер
Заголовок раздела «⚠️ Gotcha 1: Padding раздувает размер»type Bad struct { A byte // 1 B int64 // 8 (+ 7 байт padding до B) C byte // 1 (+ 7 байт padding до конца)}// = 24 байтаВ hot data (миллионы структур) важно сортировать поля по убыванию размера.
⚠️ Gotcha 2: comparable struct
Заголовок раздела «⚠️ Gotcha 2: comparable struct»Структура comparable (через ==), если все поля comparable. Slice/map/func — НЕ comparable, поэтому struct с ними тоже не comparable.
type A struct{ X int } // comparabletype B struct{ S []int } // НЕ comparablea1 := A{1}; a2 := A{1}fmt.Println(a1 == a2) // true// b1 := B{}; b2 := B{}// fmt.Println(b1 == b2) // ❌ ошибка компиляцииТолько comparable struct можно использовать как ключ map.
⚠️ Gotcha 3: Копирование — поверхностное
Заголовок раздела «⚠️ Gotcha 3: Копирование — поверхностное»type User struct{ Tags []string }u1 := User{Tags: []string{"a"}}u2 := u1 // копия struct, но Tags указывает на тот же slice!u2.Tags[0] = "X"fmt.Println(u1.Tags) // [X]!!!Если нужна глубокая копия — копируйте slice/map вручную или используйте copy().
⚠️ Gotcha 4: Method on map element — нельзя мутировать
Заголовок раздела «⚠️ Gotcha 4: Method on map element — нельзя мутировать»type Counter struct{ N int }func (c *Counter) Inc() { c.N++ }
m := map[string]Counter{"a": {0}}// m["a"].Inc() // ❌ cannot take address of m["a"]Map элементы не addressable. Решения:
- Хранить указатели:
map[string]*Counter. - Скопировать-изменить-записать обратно:
c := m["a"]; c.N++; m["a"] = c.
⚠️ Gotcha 5: Embed одно и то же имя — двусмысленность
Заголовок раздела «⚠️ Gotcha 5: Embed одно и то же имя — двусмысленность»type A struct{ X int }type B struct{ X int }type C struct{ A; B }
c := C{}// c.X = 1 // ❌ ambiguous selector c.Xc.A.X = 1 // OK⚠️ Gotcha 6: Embed nil pointer
Заголовок раздела «⚠️ Gotcha 6: Embed nil pointer»type Animal struct{ Name string }func (a *Animal) Greet() string { return "Hi " + a.Name }
type Dog struct{ *Animal }
d := Dog{} // Animal — nil!// d.Greet() // паника, обращение к nil.Name⚠️ Gotcha 7: Value receiver на slice/map — все равно мутации видны
Заголовок раздела «⚠️ Gotcha 7: Value receiver на slice/map — все равно мутации видны»type Box struct{ Items []int }func (b Box) Add() { b.Items = append(b.Items, 1) }Здесь b — копия Box, но b.Items (slice header) — это копия указателя на тот же массив. Если append не вызвал реаллокацию — снаружи изменения видны. Если вызвал — нет. Непредсказуемо! Всегда pointer receiver для методов мутации.
⚠️ Gotcha 8: JSON unmarshal в struct — только exported поля
Заголовок раздела «⚠️ Gotcha 8: JSON unmarshal в struct — только exported поля»type User struct { name string // lowercase — JSON НЕ заполнит Email string}JSON-пакет работает через reflect и видит только exported (заглавная) поля.
⚠️ Gotcha 9: Anonymous struct + сравнение
Заголовок раздела «⚠️ Gotcha 9: Anonymous struct + сравнение»a := struct{ X int }{1}b := struct{ X int }{1}fmt.Println(a == b) // true — типы идентичныНо если в одном поле дополнительный тег — типы разные:
a := struct{ X int }{1}b := struct{ X int `json:"x"` }{1}// fmt.Println(a == b) // ❌ mismatched types⚠️ Gotcha 10: Zero value struct — без явной инициализации
Заголовок раздела «⚠️ Gotcha 10: Zero value struct — без явной инициализации»var u User // все поля — zero values// u.Name == "", u.ID == 0В Go это часто валидное состояние (“zero value useful”). Но иногда вы хотите явный конструктор, который заполняет дефолты.
⚠️ Gotcha 11: Sizeof struct — не сумма Sizeof полей
Заголовок раздела «⚠️ Gotcha 11: Sizeof struct — не сумма Sizeof полей»Из-за padding и alignment, Sizeof(S) ≥ сумма размеров полей.
⚠️ Gotcha 12: Pointer на сравнение
Заголовок раздела «⚠️ Gotcha 12: Pointer на сравнение»p1 := &User{ID: 1}p2 := &User{ID: 1}fmt.Println(p1 == p2) // false — разные адресаfmt.Println(*p1 == *p2) // true — значения равны4. Производительность / Memory considerations
Заголовок раздела «4. Производительность / Memory considerations»Размер struct и cache
Заголовок раздела «Размер struct и cache»CPU кэш-линия — 64 байта. Если ваша struct ≤ 64 байт, доступ к любому полю — один cache miss. Если 128 байт — два. Поэтому “тощие” структуры лучше “жирных” в hot path.
Field alignment — практика
Заголовок раздела «Field alignment — практика»Сортируйте поля по убыванию размера:
type Optimized struct { Big int64 // 8 Med int32 // 4 Small int16 // 2 Flag bool // 1 // 1 байт паддинг до 16 (alignof самого большого поля = 8 → размер кратен 8)}Value vs pointer receiver — производительность
Заголовок раздела «Value vs pointer receiver — производительность»- Value receiver копирует struct. Для маленьких (1-2 слова) — даже быстрее (cache friendly).
- Pointer receiver — указатель (8 байт), быстрее для больших structs.
- Граница примерно: > 4 машинных слов (~32 байта) → pointer receiver.
Empty struct = 0 байт
Заголовок раздела «Empty struct = 0 байт»m := map[string]struct{}{}// каждый ключ — только сам ключ; значения нулевые байтыVs map[string]bool — bool это 1 байт + alignment → больше памяти.
chan struct{} — лучше chan bool
Заголовок раздела «chan struct{} — лучше chan bool»Для сигналов: размер передаваемого значения 0, нет лишнего байта.
Аллокации
Заголовок раздела «Аллокации»u := User{} // обычно stack (если не escape)u := &User{} // указатель → может escape в heapu := new(User) // эквивалент &User{}Pre-allocation:
users := make([]User, 0, 1000) // только один heap allocReflection и tags — дорого
Заголовок раздела «Reflection и tags — дорого»Парсинг tags на каждое поле через reflect — относительно медленный. Поэтому JSON-кодек на горячем пути может стать боттлнеком; для высокой производительности — easyjson, ffjson, или ручная сериализация.
5. Типичные вопросы на собеседовании Junior
Заголовок раздела «5. Типичные вопросы на собеседовании Junior»-
Как объявить struct?
type Name struct { Field Type; ... }. -
Что такое padding/alignment? Компилятор добавляет невидимые байты, чтобы поля начинались с адресов, кратных размеру их типа. Это ускоряет CPU-доступ.
-
Зачем сортировать поля по размеру? Минимизировать padding → уменьшить размер struct → больше элементов в cache line.
-
Что такое
struct{}и сколько он занимает? Empty struct, 0 байт. Use cases:map[K]struct{}(set),chan struct{}(сигнальный канал). -
Чем отличается value receiver от pointer receiver? Value — копия struct, не мутирует. Pointer — указатель, может мутировать, не копирует. Pointer обязателен для мутации.
-
Когда выбирать pointer receiver? Мутация полей, большая struct, контейнерные/sync поля внутри, или просто консистентность с другими методами.
-
Что такое method set? Набор методов типа.
T— только методы с receiver T.*T— методы с T и *T. Влияет на удовлетворение интерфейсов. -
Можно ли вызвать pointer-метод на значении? Если значение addressable (переменная, поле struct) — да, Go возьмёт
&автоматически. Если rvalue (вернулось из функции, элемент map) — нет. -
Что такое embedding? Анонимное поле. Методы и поля встроенного типа “промоутятся” к внешней struct. Это композиция, а не наследование.
-
Разница между embedding и обычным полем? Embedding не требует префикса
d.Animal.Name— можноd.Name. Методы тоже промоутятся. -
Что произойдёт, если в двух embedded типах одинаковое имя поля? Доступ к
c.Xбудет двусмысленным → ошибка компиляции. Нужноc.A.Xилиc.B.X. -
Когда struct comparable? Когда все поля comparable. Slice, map, func — не comparable.
-
Можно ли использовать struct как ключ map? Да, если она comparable.
-
Поверхностное или глубокое копирование struct при присваивании? Поверхностное. Slice/map/pointer поля копируют header/указатель, underlying данные общие.
-
Что такое tag в struct? Строковая аннотация поля, читается через
reflect. Используется JSON, sqlx, validator, и т.д. -
Что вернёт
unsafe.Sizeofдля struct? Размер с учётом padding (сумма полей + paddings). -
Method value и method expression — что это? Method value:
t.Method— функция с уже привязанным receiver. Method expression:T.Method— функция, принимающая T как первый аргумент. -
Что произойдёт при вызове метода на nil-указателе? Если метод не обращается к полям — отработает. Если обращается — паника. Иногда это используется намеренно (
(*List)(nil).Len()). -
Если embedded поле — указатель и nil, что будет? Обращение к промоутед методу/полю — паника.
-
Что такое constructor в Go? Нет языковой конструкции. Используется идиома: функция
NewX() *XилиNewX() (X, error).
6. Practice — мини-задачки
Заголовок раздела «6. Practice — мини-задачки»Задача 1
Заголовок раздела «Задача 1»type S struct { A bool B int64 C bool}fmt.Println(unsafe.Sizeof(S{}))Ответ
24 (1+7 паддинг + 8 + 1 + 7 паддинг). Перепаковав в `{B int64; A bool; C bool}` → 16.Задача 2
Заголовок раздела «Задача 2»Сделайте множество строк, потребляющее минимум памяти.
Ответ
```go set := map[string]struct{}{} set["a"] = struct{}{} _, ok := set["a"] ```Задача 3
Заголовок раздела «Задача 3»type T struct{ X int }func (t T) Inc() { t.X++ }
t := T{X: 1}t.Inc()fmt.Println(t.X)Ответ
`1`. Value receiver работает с копией.Задача 4
Заголовок раздела «Задача 4»Перепишите задачу 3 так, чтобы Inc действительно увеличивал X.
Ответ
```go func (t *T) Inc() { t.X++ } ```Задача 5
Заголовок раздела «Задача 5»type Animal struct{ Name string }func (a Animal) Greet() string { return "Hi " + a.Name }
type Dog struct{ Animal }
d := Dog{Animal{"Rex"}}fmt.Println(d.Greet())Ответ
`Hi Rex`. Метод промоутед.Задача 6
Заголовок раздела «Задача 6»Можно ли использовать struct{ Items []int } как ключ map?
Ответ
Нет, slice не comparable → struct не comparable.Задача 7
Заголовок раздела «Задача 7»type C struct{ N int }func (c *C) Inc() { c.N++ }
m := map[string]C{"a": {0}}// что нужно сделать чтобы Inc сработал?Ответ
Изменить на `map[string]*C` или сделать `c := m["a"]; c.N++; m["a"] = c`.Задача 8
Заголовок раздела «Задача 8»type Box struct{ Items []int }b1 := Box{Items: []int{1,2,3}}b2 := b1b2.Items[0] = 99fmt.Println(b1.Items[0])Ответ
`99`. Поверхностная копия: slice header копируется, underlying array общий.Задача 9
Заголовок раздела «Задача 9»Реализуйте Stringer для типа Point.
Ответ
```go type Point struct{ X, Y int } func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) } ```Задача 10
Заголовок раздела «Задача 10»type A struct{ X int }type B struct{ X int }type C struct { A; B }
c := C{}c.X = 1 // ?Ответ
Ошибка компиляции: ambiguous selector c.X.Задача 11
Заголовок раздела «Задача 11»Какое количество байт у struct{ a, b, c bool }?
Ответ
3 байта (1+1+1, alignof = 1). Без padding, т.к. все поля 1 байт.Задача 12
Заголовок раздела «Задача 12»Напишите NewUser(name string) *User, чтобы default CreatedAt = time.Now().
Ответ
```go type User struct{ Name string; CreatedAt time.Time } func NewUser(name string) *User { return &User{Name: name, CreatedAt: time.Now()} } ```7. Источники
Заголовок раздела «7. Источники»- Effective Go — Composition by Embedding
- Go Spec — Method sets
- Dave Cheney — “Should methods be declared on T or *T”
- Tutorial: fieldalignment
- The empty struct — Dave Cheney