Перейти к содержимому

Структуры и методы

Struct — основной агрегатный тип в Go. Без классов и наследования его дополняет embedding (композиция) и method sets. На собесе джуна спросят: “что такое padding/alignment”, “зачем struct{}”, “разница value vs pointer receiver”, “что такое method set у T и *T”. Здесь — детально, под капотом.

  1. Базовое определение и API
  2. Внутреннее устройство (ПОД КАПОТОМ)
  3. Тонкие моменты / Gotchas
  4. Производительность / Memory considerations
  5. Типичные вопросы на собеседовании Junior
  6. Practice — мини-задачки
  7. Источники

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 values
pair := struct {
X, Y int
}{1, 2}

Удобно в тестах, для temporary types, иногда в JSON-парсинге.

u.Name = "new"
fmt.Println(u.Email)
p := &u
p.Name = "via ptr" // эквивалент (*p).Name = "via ptr"

Go автоматически разыменовывает, поэтому (*p).field обычно не пишут.

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 receiver
func (c *Counter) Inc() { c.n++ } // pointer receiver

Имя получателя — короткое, обычно первая буква типа (c, u, s).

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, и т.д.

В Go нет конструкторов как языковой конструкции. Идиома — функция NewX:

func NewUser(name string) *User {
return &User{
ID: nextID(),
Name: name,
CreatedAt: time.Now(),
}
}

Поля struct лежат в памяти в порядке объявления (Go не переставляет поля). Но между ними может вставляться padding для выравнивания (alignment).

type Bad struct {
A byte // 1 байт
B int64 // 8 байт (требует выравнивания по 8)
C byte // 1 байт
}

Без выравнивания:

A | B | C
1 | 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{})) // 24
fmt.Println(unsafe.Sizeof(Good{})) // 16
fmt.Println(unsafe.Alignof(int64(0))) // 8
fmt.Println(unsafe.Offsetof(Bad{}.B)) // 8

Утилита fieldalignment от golang.org/x/tools подсказывает оптимальный порядок:

Окно терминала
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest
fieldalignment ./...
var a, b struct{}
fmt.Println(&a == &b) // не специфицировано, может быть true
fmt.Println(unsafe.Sizeof(a)) // 0

Runtime использует один и тот же адрес для всех значений struct{} (zerobase).

func (c Counter) Get() int { return c.n } // получает копию Counter
func (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
TВсе методы с receiver T (value)
*TВсе методы с receiver T и *T
type Greeter interface{ Greet() }
type Hello struct{}
func (h Hello) Greet() {} // value receiver
var _ Greeter = Hello{} // OK
var _ Greeter = &Hello{} // OK (через *T)
type Hi struct{}
func (h *Hi) Greet() {} // pointer receiver
var _ 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).

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()) // 10
t.X = 20
fmt.Println(mv()) // 10 (потому что внутри mv хранится копия t)
me := T.Get // method expression
fmt.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.Get
c.X = 2
fmt.Println(mv()) // 2

Embedding — это просто анонимное поле. Промоушн методов — синтаксический сахар:

type Dog struct {
Animal
Breed string
}
// d.Greet() == d.Animal.Greet()
// d.Name == d.Animal.Name

Это композиция, не наследование. Embedded type не знает о Dog. Нельзя “переопределить” метод так, чтобы вызов из Animal попал в Dog’овский (нет полиморфизма базовый-вызывает-производный).

type A struct{ X int }
type B struct{ A } // value embed
type C struct{ *A } // pointer embed
b := B{A: A{X: 1}}
c := C{A: &A{X: 1}}

Pointer embed экономит копирование при включении большой структуры; даёт nil как валидное “ничего”.

type T struct {
F int `json:"f" db:"f_col"`
}

json пакет через reflect.StructTag.Get("json") парсит тэги. Format: key:"value" через пробел.


type Bad struct {
A byte // 1
B int64 // 8 (+ 7 байт padding до B)
C byte // 1 (+ 7 байт padding до конца)
}
// = 24 байта

В hot data (миллионы структур) важно сортировать поля по убыванию размера.

Структура comparable (через ==), если все поля comparable. Slice/map/func — НЕ comparable, поэтому struct с ними тоже не comparable.

type A struct{ X int } // comparable
type B struct{ S []int } // НЕ comparable
a1 := A{1}; a2 := A{1}
fmt.Println(a1 == a2) // true
// b1 := B{}; b2 := B{}
// fmt.Println(b1 == b2) // ❌ ошибка компиляции

Только comparable struct можно использовать как ключ map.

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.X
c.A.X = 1 // OK
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 (заглавная) поля.

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”). Но иногда вы хотите явный конструктор, который заполняет дефолты.

Из-за padding и alignment, Sizeof(S) ≥ сумма размеров полей.

p1 := &User{ID: 1}
p2 := &User{ID: 1}
fmt.Println(p1 == p2) // false — разные адреса
fmt.Println(*p1 == *p2) // true — значения равны

CPU кэш-линия — 64 байта. Если ваша struct ≤ 64 байт, доступ к любому полю — один cache miss. Если 128 байт — два. Поэтому “тощие” структуры лучше “жирных” в hot path.

Сортируйте поля по убыванию размера:

type Optimized struct {
Big int64 // 8
Med int32 // 4
Small int16 // 2
Flag bool // 1
// 1 байт паддинг до 16 (alignof самого большого поля = 8 → размер кратен 8)
}
  • Value receiver копирует struct. Для маленьких (1-2 слова) — даже быстрее (cache friendly).
  • Pointer receiver — указатель (8 байт), быстрее для больших structs.
  • Граница примерно: > 4 машинных слов (~32 байта) → pointer receiver.
m := map[string]struct{}{}
// каждый ключ — только сам ключ; значения нулевые байты

Vs map[string]bool — bool это 1 байт + alignment → больше памяти.

Для сигналов: размер передаваемого значения 0, нет лишнего байта.

u := User{} // обычно stack (если не escape)
u := &User{} // указатель → может escape в heap
u := new(User) // эквивалент &User{}

Pre-allocation:

users := make([]User, 0, 1000) // только один heap alloc

Парсинг tags на каждое поле через reflect — относительно медленный. Поэтому JSON-кодек на горячем пути может стать боттлнеком; для высокой производительности — easyjson, ffjson, или ручная сериализация.


  1. Как объявить struct? type Name struct { Field Type; ... }.

  2. Что такое padding/alignment? Компилятор добавляет невидимые байты, чтобы поля начинались с адресов, кратных размеру их типа. Это ускоряет CPU-доступ.

  3. Зачем сортировать поля по размеру? Минимизировать padding → уменьшить размер struct → больше элементов в cache line.

  4. Что такое struct{} и сколько он занимает? Empty struct, 0 байт. Use cases: map[K]struct{} (set), chan struct{} (сигнальный канал).

  5. Чем отличается value receiver от pointer receiver? Value — копия struct, не мутирует. Pointer — указатель, может мутировать, не копирует. Pointer обязателен для мутации.

  6. Когда выбирать pointer receiver? Мутация полей, большая struct, контейнерные/sync поля внутри, или просто консистентность с другими методами.

  7. Что такое method set? Набор методов типа. T — только методы с receiver T. *T — методы с T и *T. Влияет на удовлетворение интерфейсов.

  8. Можно ли вызвать pointer-метод на значении? Если значение addressable (переменная, поле struct) — да, Go возьмёт & автоматически. Если rvalue (вернулось из функции, элемент map) — нет.

  9. Что такое embedding? Анонимное поле. Методы и поля встроенного типа “промоутятся” к внешней struct. Это композиция, а не наследование.

  10. Разница между embedding и обычным полем? Embedding не требует префикса d.Animal.Name — можно d.Name. Методы тоже промоутятся.

  11. Что произойдёт, если в двух embedded типах одинаковое имя поля? Доступ к c.X будет двусмысленным → ошибка компиляции. Нужно c.A.X или c.B.X.

  12. Когда struct comparable? Когда все поля comparable. Slice, map, func — не comparable.

  13. Можно ли использовать struct как ключ map? Да, если она comparable.

  14. Поверхностное или глубокое копирование struct при присваивании? Поверхностное. Slice/map/pointer поля копируют header/указатель, underlying данные общие.

  15. Что такое tag в struct? Строковая аннотация поля, читается через reflect. Используется JSON, sqlx, validator, и т.д.

  16. Что вернёт unsafe.Sizeof для struct? Размер с учётом padding (сумма полей + paddings).

  17. Method value и method expression — что это? Method value: t.Method — функция с уже привязанным receiver. Method expression: T.Method — функция, принимающая T как первый аргумент.

  18. Что произойдёт при вызове метода на nil-указателе? Если метод не обращается к полям — отработает. Если обращается — паника. Иногда это используется намеренно ((*List)(nil).Len()).

  19. Если embedded поле — указатель и nil, что будет? Обращение к промоутед методу/полю — паника.

  20. Что такое constructor в Go? Нет языковой конструкции. Используется идиома: функция NewX() *X или NewX() (X, error).


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.

Сделайте множество строк, потребляющее минимум памяти.

Ответ ```go set := map[string]struct{}{} set["a"] = struct{}{} _, ok := set["a"] ```
type T struct{ X int }
func (t T) Inc() { t.X++ }
t := T{X: 1}
t.Inc()
fmt.Println(t.X)
Ответ `1`. Value receiver работает с копией.

Перепишите задачу 3 так, чтобы Inc действительно увеличивал X.

Ответ ```go func (t *T) Inc() { t.X++ } ```
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`. Метод промоутед.

Можно ли использовать struct{ Items []int } как ключ map?

Ответ Нет, slice не comparable → struct не comparable.
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`.
type Box struct{ Items []int }
b1 := Box{Items: []int{1,2,3}}
b2 := b1
b2.Items[0] = 99
fmt.Println(b1.Items[0])
Ответ `99`. Поверхностная копия: slice header копируется, underlying array общий.

Реализуйте Stringer для типа Point.

Ответ ```go type Point struct{ X, Y int } func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) } ```
type A struct{ X int }
type B struct{ X int }
type C struct { A; B }
c := C{}
c.X = 1 // ?
Ответ Ошибка компиляции: ambiguous selector c.X.

Какое количество байт у struct{ a, b, c bool }?

Ответ 3 байта (1+1+1, alignof = 1). Без padding, т.к. все поля 1 байт.

Напишите 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()} } ```

  1. Effective Go — Composition by Embedding
  2. Go Spec — Method sets
  3. Dave Cheney — “Should methods be declared on T or *T”
  4. Tutorial: fieldalignment
  5. The empty struct — Dave Cheney