Содержание

Структуры в Go

Структуры позволяют собрать связанные данные в один тип. Если переменные - это отдельные кирпичи, то структура - это маленькая модель объекта: пользователь, книга, заказ, конфиг, результат парсинга, запись лога.

Без структур код быстро превращается в набор разрозненных переменных:

userID := 1 userName := "Alice" userEmail := "alice@example.com" userAge := 30

Пока данных мало, это терпимо. Но как только нужно передать пользователя в функцию, сохранить список пользователей или вернуть его из API, лучше создать тип.

Определение структуры

type User struct { ID int Name string Age int Email string }

type User struct объявляет новый тип. Внутри перечислены поля и их типы.

Теперь User можно использовать как обычный тип:

func printUser(user User) { fmt.Println(user.Name, user.Email) }

Создание значения

Чаще всего используют именованные поля:

user := User{ ID: 1, Name: "Alice", Age: 30, Email: "alice@example.com", }

Такой код чуть длиннее, зато устойчив к изменению порядка полей. Если вы позже добавите новое поле, старый код останется понятным.

Есть короткая форма:

user := User{1, "Alice", 30, "alice@example.com"}

В учебных примерах она встречается, но в реальных проектах ее используют редко: нужно помнить порядок всех полей.

Доступ к полям

К полям обращаются через точку:

fmt.Println(user.Name) user.Age = 31

Если структура лежит в указателе, синтаксис остается таким же:

userPtr := &user userPtr.Name = "Alicia"

Go сам разыменует указатель для доступа к полю.

Нулевые значения

Если создать структуру без заполнения, поля получат нулевые значения:

var user User fmt.Println(user.ID) // 0 fmt.Println(user.Name) // "" fmt.Println(user.Age) // 0

Нулевые значения полезны, но иногда могут скрыть ошибку. Например, Age: 0 может означать и "возраст неизвестен", и "младенец". Если это важно, используйте pointer или отдельный признак.

Методы

К структурам можно добавлять методы:

func (u User) DisplayName() string { if u.Name == "" { return "anonymous" } return u.Name }

Метод вызывается так:

fmt.Println(user.DisplayName())

Метод похож на функцию, но связан с типом. Receiver (u User) показывает, для какого типа этот метод определен.

Value receiver и pointer receiver

Если метод только читает данные, можно использовать value receiver:

func (u User) IsAdult() bool { return u.Age >= 18 }

Если метод должен менять структуру, нужен pointer receiver:

func (u *User) Rename(name string) { u.Name = name }

Без указателя метод получил бы копию структуры и изменил бы только копию.

Хорошее правило: если у типа есть методы с pointer receiver, часто остальные методы тоже делают pointer receiver для единообразия.

Вложенные структуры

Структуры могут содержать другие структуры:

type Address struct { City string Street string } type User struct { ID int Name string Address Address }

Доступ:

fmt.Println(user.Address.City)

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

Embedding

Go поддерживает встраивание:

type Timestamps struct { CreatedAt time.Time UpdatedAt time.Time } type Book struct { ID int Title string Timestamps }

Теперь можно обращаться так:

fmt.Println(book.CreatedAt)

Embedding не является наследованием как в классических ООП-языках. Это способ встроить поля и методы одного типа в другой.

Struct tags

Tags добавляют метаданные к полям. Частый пример - JSON:

type User struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email,omitempty"` }

Пакет encoding/json читает эти tags и понимает, как назвать поля в JSON.

Важно: JSON видит только экспортируемые поля, то есть поля с большой буквы:

type User struct { name string `json:"name"` // не попадет в JSON }

Структуры в API

Не всегда стоит использовать одну структуру для базы, API-запроса и API-ответа. Часто лучше разделить:

type CreateUserRequest struct { Name string `json:"name"` Email string `json:"email"` } type UserResponse struct { ID int `json:"id"` Name string `json:"name"` Email string `json:"email"` }

Так вы не случайно отправите клиенту внутренние поля, например PasswordHash.

Частые ошибки

  1. Использовать короткий литерал без имен полей в большом типе.
  2. Ожидать, что value receiver изменит исходную структуру.
  3. Делать одну структуру для всех слоев приложения.
  4. Забывать экспортировать поля для JSON.
  5. Путать embedding с наследованием.
  6. Хранить слишком много несвязанных полей в одном типе.

Чеклист

  1. Связанные данные объединены в понятный тип.
  2. Литералы используют имена полей.
  3. Методы, которые изменяют значение, имеют pointer receiver.
  4. Для JSON поля экспортированы и имеют tags.
  5. Request и response структуры не раскрывают внутренние секреты.
  6. Вложенные структуры используются там, где есть реальная смысловая связь.

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи