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

ОСНОВЫ ЯЗЫКА: GO Структуры и методы

Обязательно

Структуры и методы в Go: данные, поведение и композиция

Go

Как в Go устроены struct, их поля и инициализация, методы с value и pointer receiver, и почему вместо наследования здесь используют встраивание структур.

1. struct хранит именованные поля

1 / 5

Тип User объявляет собственный набор данных, без классов и скрытого состояния. Поля с заглавной буквы видны другим пакетам.

  • Инициализируйте поля по имени: User{Name: "Анна"}.
  • Позиционный литерал хрупок при добавлении полей.
  • Незаданные поля получают нулевое значение; NewUser - обычная функция-конструктор.
type User struct ID int Name string Email string Admin bool User{}: 0, "", "", false

2. Метод получает receiver

2 / 5

Между func и именем метода стоит receiver - значение своего типа. Методы объявляют только для типов своего пакета, не обязательно struct.

  • func (u User) Greeting() string - метод на User.
  • Вызывается через точку: u.Greeting().
  • Receiver можно сделать и для type Celsius float64.
u.Userreceiver Greeting()return "Привет, " + u.Name u.Greeting()

3. Value копирует, pointer меняет оригинал

3 / 5

Value receiver работает с копией структуры, pointer receiver - с оригиналом. Если меняете состояние или структура большая, выбирайте pointer.

  • RenameCopy теряет изменение.
  • Rename сохраняет его.
  • Не смешивайте receiver-ы у одного типа: pointer-методы входят в набор *User, поэтому и интерфейс реализует указатель.
UserName copy: Анна pointer: Борис Состояние меняем - pointer receiver

4. Встраивание поднимает поля и методы

4 / 5

Вместо наследования используют композицию и embedding. Встроенный тип не является родителем: внешняя структура содержит его и делегирует вызовы.

  • Address внутри Customer позволяет писать c.City.
  • c.FullAddress() вызывает метод встроенного Address.
  • При конфликте имен внешнее поле выигрывает; к встроенному обращаются c.Address.City.
Customer AddressCity · FullAddress() c.City c.FullAddress()

5. Копия не всегда глубокая, теги меняют JSON

5 / 5

Срез внутри структуры - окно в общий массив, поэтому даже value receiver может изменить данные оригинала. Для честной копии используют make и copy.

  • items := make([]string, len(o.Items)); copy(items, o.Items).
  • == работает, только если все поля сравнимы; срез, map и функция делают тип несравнимым.
  • Тег json:"name" читает библиотека, omitempty убирает пустое поле.
items[0:2]общий массив copy(items)новый массив json:"name" json:"email,omitempty"

В 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 для указателей и срезов — что это такое, расскажет статья про срезы и map, пока достаточно запомнить само слово. Пустая структура — это тоже валидное значение:

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) получает копию структуры. Изменения внутри метода не видны снаружи — меняется копия. Pointer receiver (u *User) получает указатель на оригинал, и изменения через него видны снаружи:

живой пример

package main

import "fmt"

type User struct {
    Name string
}

func (u User) RenameCopy(name string) {
    u.Name = name // меняется копия, оригинал не тронут
}

func (u *User) Rename(name string) {
    u.Name = name // меняется оригинал
}

func main() {
    u := User{Name: "Анна"}

    u.RenameCopy("Борис")
    fmt.Println(u.Name) // Анна — изменение потерялось

    u.Rename("Борис")
    fmt.Println(u.Name) // Борис
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Обратите внимание: вызывать u.Rename(...) можно и на обычной переменной u (не указателе) — Go автоматически берёт адрес, если переменную можно адресовать. Синтаксис вызова одинаковый; разница только в объявлении метода. Подробнее про сам механизм указателей — в статье про указатели.

Два уточнения, без которых задачи на этот раздел не решаются. Первое: receiver работает с полями через точку, но если тип - не структура, а число (type Count int), точку применить не к чему - указатель разыменовывают явно звёздочкой: func (c *Count) Increment() { *c++ }. Второе: копия структуры не всегда глубокая. Слайс внутри структуры - это «окно» в общий массив: и value receiver, и присваивание копируют окно, а не данные, и правка через копию видна в оригинале. Честная копия делается руками: items := make([]string, len(o.Items)); copy(items, o.Items). Полный разбор того, что у среза внутри - в статье Срезы и map.

Короткая формула выбора:

  • Нужно менять состояние структуры или структура большая (копировать дорого) → 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) // обращаемся через имя поля

Теперь встраивание — то же поле, но без имени:

живой пример

package main

import "fmt"

type Address struct {
    City   string
    Street string
}

func (a Address) FullAddress() string {
    return a.City + ", " + a.Street
}

type Customer struct {
    Name    string
    Address // встроенный тип, без имени поля
}

func main() {
    c := Customer{Name: "Анна", Address: Address{City: "Москва", Street: "Тверская"}}
    fmt.Println(c.City)          // поля Address доступны напрямую!
    fmt.Println(c.FullAddress()) // и методы тоже
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

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