В 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 у всех методов этого типа. Так предсказуемее и так требуют интерфейсы.
Композиция вместо наследования: встраивание
В 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 это основной способ переиспользования, и в связке с интерфейсами он закрывает почти все задачи, для которых в других языках берут иерархии классов.
Коротко
- struct — именованный набор полей; классов в Go нет.
- Инициализируйте поля по имени (
User{Name: ...}); незаданные поля получают нулевое значение. - Нетривиальное создание оформляют функцией-конструктором
NewX— это соглашение, а не синтаксис языка. - Метод — функция с receiver между
funcи именем; объявляется только для типов своего пакета. - Value receiver работает с копией (изменения теряются), pointer receiver — с оригиналом (изменения сохраняются).
- Выбор: меняете состояние или структура большая → pointer; и не смешивайте receiver-ы у одного типа.
- Наследования нет — переиспользование через композицию и встраивание (embedding), которое поднимает поля и методы встроенного типа наверх.
Что почитать дальше
- Синтаксис и типы — базовые типы, переменные и правила видимости по регистру.
- Указатели — как устроены
&и*, почему pointer receiver меняет оригинал. - Интерфейсы — как типы со своими методами начинают удовлетворять интерфейсам без явного объявления.