← назад к разделу

В 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 меняет оригинал.
  • Интерфейсы — как типы со своими методами начинают удовлетворять интерфейсам без явного объявления.