В Go нет классов. Там, где в других языках вы бы написали class, в Go есть struct — простой набор именованных полей — и методы, которые к этому набору привязываются. Разберём, как объединять данные, как навешивать на них поведение и почему вместо наследования здесь обходятся композицией.
Структура: набор именованных полей
struct — это тип, который группирует несколько значений (полей) под одним именем. Объявляется через type и struct:
type User struct {
ID int
Name string
Email string
Admin bool
}
Здесь User — новый тип. У него четыре поля, у каждого своё имя и тип. Никакого скрытого состояния, наследования или конструктора по умолчанию — только то, что вы написали.
Поля начинаются с заглавной буквы (Name) или строчной (name). Это не стиль, а правило видимости: имя с заглавной буквы экспортируется — видно из других пакетов; со строчной — приватно для своего пакета. Подробнее об этом — в синтаксисе и типах.
Создание и инициализация
Структуру можно создать несколькими способами. Самый явный и читаемый — указать поля по имени:
u := User{
ID: 1,
Name: "Анна",
Email: "anna@example.com",
Admin: false,
}
Можно перечислить значения позиционно, без имён, — но так делать не стоит. Позиционный литерал обязан перечислить все поля, поэтому новое поле сломает сборку во всех местах, где он написан, а если поменять местами два поля одного типа, значения молча встанут не туда — этого компилятор уже не увидит.
u := User{1, "Анна", "anna@example.com", false} // хрупко, не делайте так
Если какие-то поля не указать, они получат нулевое значение своего типа: 0 для чисел, "" для строк, false для bool, nil для указателей и слайсов. Пустая структура — это тоже валидное значение:
var u User // все поля в нуле: ID=0, Name="", Email="", Admin=false
u2 := User{} // то же самое, но явно
Часто структуру создают и сразу берут на неё указатель через &:
u := &User{Name: "Анна"} // u имеет тип *User
Когда инициализация нетривиальна (есть проверки, значения по умолчанию), в Go по соглашению пишут функцию-конструктор с префиксом New:
func NewUser(name, email string) *User {
return &User{
Name: name,
Email: email,
Admin: false, // по умолчанию обычный пользователь
}
}
Это обычная функция, а не специальный синтаксис. Язык не навязывает конструкторов — это просто удобный приём.
Доступ к полям — через точку, как и ожидается:
u.Name = "Борис"
fmt.Println(u.Email)
Методы и receiverспросят на собеседовании
Метод — это функция, привязанная к типу. Отличие от обычной функции одно: между func и именем стоит receiver — переменная, через которую метод получает значение своего типа.
func (u User) Greeting() string {
return "Привет, " + u.Name
}
(u User) — это и есть receiver. Внутри метода u ведёт себя как обычный параметр. Вызывается метод через точку:
u := User{Name: "Анна"}
fmt.Println(u.Greeting()) // Привет, Анна
Методы можно объявлять только для типов, определённых в вашем пакете. Зато не обязательно для struct — receiver может быть у любого вашего типа, например над type Celsius float64.
Value receiver и pointer receiverспросят на собеседовании
Это самое важное различие в теме. Receiver бывает двух видов, и выбор между ними меняет поведение.
Value receiver (u User) получает копию структуры. Изменения внутри метода не видны снаружи — меняется копия:
func (u User) Rename(name string) {
u.Name = name // меняется копия, оригинал не тронут
}
u := User{Name: "Анна"}
u.Rename("Борис")
fmt.Println(u.Name) // Анна — изменение потерялось
Pointer receiver (u *User) получает указатель на оригинал. Изменения через него видны снаружи:
func (u *User) Rename(name string) {
u.Name = name // меняется оригинал
}
u := User{Name: "Анна"}
u.Rename("Борис")
fmt.Println(u.Name) // Борис
Обратите внимание: вызывать u.Rename(...) можно и на обычной переменной u (не указателе) — Go автоматически берёт адрес, если переменную можно адресовать. Синтаксис вызова одинаковый; разница только в объявлении метода. Подробнее про сам механизм указателей — в статье про указатели.
Короткая формула выбора:
- Нужно менять состояние структуры или структура большая (копировать дорого) → pointer receiver.
- Только читаете и структура маленькая → можно value receiver.
- Главное — не смешивать. Если хоть один метод типа использует pointer receiver, делайте pointer receiver у всех методов этого типа. Причина не только в предсказуемости: методы с pointer receiver входят в набор методов
*User, но неUser, поэтому значениеUserне подойдёт под интерфейс, которому нужен хотя бы один такой метод, — туда придётся класть указатель.
Композиция вместо наследования: встраиваниеспросят на собеседовании
В Go нет наследования классов — нет extends. Переиспользование строится через композицию: одну структуру вкладывают в другую. И есть особая форма — встраивание (embedding), когда поле записывают без имени, только тип.
Сначала обычная композиция — именованное поле:
type Address struct {
City string
Street string
}
type Customer struct {
Name string
Address Address // именованное поле
}
c := Customer{Name: "Анна", Address: Address{City: "Москва"}}
fmt.Println(c.Address.City) // обращаемся через имя поля
Теперь встраивание — то же поле, но без имени:
type Customer struct {
Name string
Address // встроенный тип, без имени поля
}
c := Customer{Name: "Анна", Address: Address{City: "Москва"}}
fmt.Println(c.City) // поля Address доступны напрямую!
Встроенный тип поднимает свои поля и методы на уровень внешней структуры. Если у Address есть метод FullAddress(), его можно звать как c.FullAddress() — будто он определён у Customer. Это похоже на наследование, но это не оно: Customer не «является» Address, он его содержит и делегирует. При конфликте имён выигрывает поле внешней структуры, а к встроенному всегда можно обратиться по имени типа — c.Address.City.
Почему так, а не наследование? Композиция даёт меньше связанности: вы собираете поведение из независимых кусочков, а не наследуете весь груз родителя. В Go это основной способ переиспользования, и в связке с интерфейсами он закрывает почти все задачи, для которых в других языках берут иерархии классов.
Глубже: теги полей, сравнение и анонимные структурырасширенное
Рядом с типом поля можно записать тег — строку в обратных кавычках. Язык её не трактует, зато читают библиотеки через рефлексию. Самый частый случай — имена полей в JSON:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // пустое поле в JSON не попадёт
}
Без тегов encoding/json возьмёт имена полей как есть, с заглавной буквы, а поля со строчной буквы не увидит вовсе — они не экспортируются. Как это работает в обработчиках, разобрано в обработчиках и JSON.
Структуры сравниваются через == целиком, поле за полем, — если все поля сравнимы. Срез, map и функция внутри делают тип несравнимым, и == для него не соберётся; такие структуры сравнивают вручную или через reflect.DeepEqual в тестах. Сравнимая структура годится и ключом map.
Наконец, структуру можно объявить на месте, без имени типа: point := struct{ X, Y int }{1, 2}. Так удобно для одноразовых значений — например, тела ответа в тесте или набора табличных случаев.
Коротко
- struct — именованный набор полей; классов в Go нет.
- Инициализируйте поля по имени (
User{Name: ...}); незаданные поля получают нулевое значение. - Нетривиальное создание оформляют функцией-конструктором
NewX— это соглашение, а не синтаксис языка. - Метод — функция с receiver между
funcи именем; объявляется только для типов своего пакета. - Value receiver работает с копией (изменения теряются), pointer receiver — с оригиналом (изменения сохраняются).
- Выбор: меняете состояние или структура большая → pointer; и не смешивайте receiver-ы у одного типа.
- Наследования нет — переиспользование через композицию и встраивание (embedding), которое поднимает поля и методы встроенного типа наверх.
- Тег поля (
`json:"name"`) читают библиотеки, а не язык; структура сравнима через==, если сравнимы все её поля.
Что пощупать
Тип Product из учебного каталога практикума remodov/marketplace-system-go: поля не экспортированы, состояние меняется только методами, и правило «скидка не глубже половины» живёт в самом типе, а не в сервисе.
Код: product.go.
Сделаем сами
Ветка step-04-domain-rules.
Что почитать дальше
- Синтаксис и типы — базовые типы, переменные и правила видимости по регистру.
- Указатели — как устроены
&и*, почему pointer receiver меняет оригинал. - Интерфейсы — как типы со своими методами начинают удовлетворять интерфейсам без явного объявления.