Структуры в 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.
Частые ошибки
- Использовать короткий литерал без имен полей в большом типе.
- Ожидать, что value receiver изменит исходную структуру.
- Делать одну структуру для всех слоев приложения.
- Забывать экспортировать поля для JSON.
- Путать embedding с наследованием.
- Хранить слишком много несвязанных полей в одном типе.
Чеклист
- Связанные данные объединены в понятный тип.
- Литералы используют имена полей.
- Методы, которые изменяют значение, имеют pointer receiver.
- Для JSON поля экспортированы и имеют tags.
- Request и response структуры не раскрывают внутренние секреты.
- Вложенные структуры используются там, где есть реальная смысловая связь.