Структура в Go объединяет именованные поля в одно значение. Через struct можно описать пользователя, настройки сервера или ключ кеша. Структуры копируются при присваивании, поддерживают методы и встраивание других типов. Сравнение через == работает, только если сравнимы все поля.
Разберём создание структуры, передачу по значению и через указатель, встраивание полей и методов. Отдельно посмотрим, почему копия со срезом меняет исходные данные и когда сравнение двух структур приводит к панике.
Как объявить структуру в Go
Начнём с пользователя. У него есть идентификатор, имя и признак активности:
package main
import "fmt"
type User struct {
ID int
Name string
Active bool
}
func main() {
u := User{
ID: 42,
Name: "Анна",
Active: true,
}
fmt.Println(u.Name) // Анна
}
type User struct объявляет тип User. В фигурных скобках перечислены поля и их типы. Значение User{...} называется составным литералом. В нём мы заполняем поля, а через точку читаем их: u.Name.
Менять поле можно так же: u.Active = false. Новое значение должно подходить типу поля. Присвоить строку полю ID типа int не получится, компилятор сообщит об ошибке.
Имя типа и имена его полей экспортируются независимо. User доступен из другого пакета, потому что начинается с заглавной буквы. Поле Name тоже доступно. Поле passwordHash с маленькой буквы было бы доступно только внутри пакета, где объявлена структура.
Это граница пакета, а не отдельного файла. Два файла одного пакета могут обращаться к его неэкспортируемым полям. Теги JSON эту границу не отменяют: тег сам по себе не откроет закрытое поле для encoding/json.
Дальше в коротких примерах опущены package main, импорты и функция main. Объявления типов и методов размещайте на уровне пакета, вызовы и присваивания внутри main. Для вывода подключите fmt; остальные импорты указаны в тексте.
Создание структуры и нулевое значение
Чтобы получить структуру, не обязательно заполнять её поля:
var u User
fmt.Printf("%+v\n", u) // {ID:0 Name: Active:false}
u.Name = "Анна"
Каждое поле получает нулевое значение своего типа: число становится 0, строка пустой, логическое значение равно false. Указатели, срезы и карты получают nil. Поле другой структуры, в свою очередь, получает структуру с нулевыми значениями полей.
Запись User{} тоже создаёт нулевое значение User. Это обычная структура, с которой уже можно работать. Например, читать Name безопасно: вы получите пустую строку.
Литералы с именами полей и без них
В литерале разрешено указать только часть полей:
u := User{Name: "Анна"}
fmt.Println(u.ID, u.Active) // 0 false
Пропущенные поля сохранят нулевые значения. Имена полей делают код понятным без перехода к объявлению типа.
Можно перечислить значения по порядку:
u := User{42, "Анна", true}
Но теперь порядок привязан к объявлению User. Если добавить поле, такой литерал придётся исправить. Если поменять местами два поля одного типа, код может продолжить компилироваться и записывать данные не туда.
Поэтому для прикладных структур я бы начинал с именованных полей. Особенно когда литерал находится далеко от объявления типа. Кроме того, литерал чужого пакета нельзя заполнить позиционно, если в структуре есть неэкспортируемые поля.
Указатель на структуру
&User{...} создаёт значение и возвращает указатель на него:
u := &User{Name: "Анна"}
u.Active = true
fmt.Println(u.Name, u.Active) // Анна true
Тип переменной u здесь *User. В выражении u.Name Go автоматически разыменовывает указатель. Запись (*u).Name тоже допустима, но обычно избыточна.
new(User) возвращает указатель на нулевое значение User. Для этого случая результат аналогичен &User{}. Если поля нужно заполнить сразу, литерал короче:
u := new(User)
u.Name = "Анна"
v := &User{Name: "Анна"}
fmt.Println(u.Name == v.Name) // true
Указатель тоже имеет нулевое значение, но это уже nil:
var u *User
fmt.Println(u == nil) // true
// fmt.Println(u.Name) // Паника: разыменование nil.
Не путайте var u User и var u *User. В первом случае пользователь уже есть, во втором указатель пока ни на что не указывает.
Синтаксис new или & сам по себе не говорит, что данные попадут в кучу. Размещение зависит от анализа компилятора. Подробнее об этом в статье про escape analysis.
Копирование структуры: где заканчивается независимость
Присваивание структуры копирует её значение:
first := User{ID: 42, Name: "Анна"}
second := first
second.Name = "Борис"
fmt.Println(first.Name) // Анна
fmt.Println(second.Name) // Борис
first и second содержат отдельные поля Name. Изменение копии не меняет оригинал. Передача структуры в функцию тоже копирует значение. Это общее правило передачи аргументов в Go, которое разобрано в статье о функциях.
Однако копирование структуры не создаёт независимые копии всех данных, доступных через её поля. Посмотрим на срез:
type Team struct {
Name string
Members []string
}
// Внутри main:
first := Team{Name: "API", Members: []string{"Анна"}}
second := first
second.Name = "Поддержка"
second.Members[0] = "Борис"
fmt.Println(first.Name) // API
fmt.Println(first.Members[0]) // Борис
Копия поля Name независима от оригинала. В копии поля Members остаётся ссылка на базовый массив. Оба среза пока обращаются к одному массиву. Запись по индексу меняет общий элемент.
С картой ситуация похожая: копия поля map обращается к той же карте. Копия указателя указывает на тот же объект. Поэтому фраза «структура передаётся по значению» не означает «вложенные данные полностью изолированы».
Если нужна отдельная копия списка участников, скопируйте срез явно. Здесь используется пакет slices:
second := first
second.Members = slices.Clone(first.Members)
second.Members[0] = "Вера"
fmt.Println(first.Members[0]) // Борис
Для []string этого достаточно, чтобы независимо заменять элементы. Но если бы элементы были указателями, slices.Clone скопировал бы указатели, а объекты по ним остались бы общими. Правило копирования нужно выбирать по устройству данных.
Отдельная осторожность нужна со структурами, содержащими sync.Mutex и другие примитивы синхронизации. Например, sync.Mutex нельзя копировать после первого использования. Передавать такую структуру по значению без разбора опасно.
Методы структуры и указатель в получателе
Метод связан с типом через получателя перед именем функции:
func (u User) DisplayName() string {
return u.Name
}
func (u *User) Rename(name string) {
u.Name = name
}
У DisplayName получатель имеет тип User. При вызове метод получает копию значения. У Rename получатель *User, поэтому метод меняет исходную структуру:
u := User{Name: "Анна"}
u.Rename("Вера")
fmt.Println(u.DisplayName()) // Вера
Переменная u имеет тип User, но вызов u.Rename(...) допустим. Поскольку у переменной можно взять адрес, компилятор обрабатывает вызов как (&u).Rename(...).
Эта помощь работает не везде. Например, у элемента карты нельзя взять адрес:
users := map[int]User{42: {Name: "Анна"}}
// users[42].Rename("Вера") // Ошибка компиляции.
u := users[42]
u.Rename("Вера")
users[42] = u
Другой вариант хранения: map[int]*User. Тогда метод можно вызвать через указатель в карте, но перед обращением нужно убедиться, что он не равен nil.
Указатель нужен, если метод должен менять саму структуру. Это не универсальный способ «ускорить код». Передача указателя меняет доступ к данным, а решение о производительности требует измерений.
Есть и различие при реализации интерфейсов. Метод Rename входит в набор методов *User, но не User. Автоматическое взятие адреса при вызове метода не делает значение User реализацией интерфейса с методом Rename(string).
Вложенное поле и встраивание
Одна структура может содержать другую как обычное именованное поле:
type Address struct {
City string
}
type Customer struct {
Name string
Address Address
}
// Внутри main:
c := Customer{
Name: "Анна",
Address: Address{City: "Казань"},
}
fmt.Println(c.Address.City) // Казань
Здесь путь к городу проходит через поле Address. Можно пойти другим путём и встроить тип, не задавая отдельное имя поля:
type Customer struct {
Name string
Address
}
// Внутри main:
c := Customer{
Name: "Анна",
Address: Address{City: "Казань"},
}
fmt.Println(c.City) // Казань
fmt.Println(c.Address.City) // Казань
Это альтернативное объявление Customer, его не нужно добавлять рядом с предыдущим. Встроенное поле получает имя Address, а его поле City становится доступно через сокращённый селектор c.City. В спецификации такие поля называют promoted fields.
Сокращение селектора не превращает City в непосредственно объявленное поле Customer. Поэтому литерал Customer{City: "Казань"} не скомпилируется. Заполняйте вложенное значение через Address: Address{...}.
Встраивание методов
Встраивание распространяется и на методы:
func (a Address) Label() string {
return a.City
}
// Внутри main, Customer содержит встроенный Address:
c := Customer{Address: Address{City: "Казань"}}
fmt.Println(c.Label()) // Казань
Вызов обращается к методу встроенного Address. Однако Customer остаётся отдельным типом. Передать его в функцию с параметром Address нельзя: передайте c.Address.
Встраивание в Go не создаёт наследования классов с виртуальным переопределением. Если объявить у Customer свой метод Label, выражение c.Label() выберет его. Вызов c.Address.Label() по-прежнему обратится к методу Address.
Получатель метода Address не становится Customer. Если метод встроенного типа вызывает другой свой метод, одноимённый метод внешней структуры не подменит этот вызов автоматически.
Конфликты имён и встроенные указатели
Если на одной глубине доступны два поля с одинаковым именем, сокращённый селектор неоднозначен:
type Billing struct {
City string
}
type Shipping struct {
City string
}
type Order struct {
Billing
Shipping
}
// Внутри main:
o := Order{}
// o.City = "Казань" // Ошибка: неоднозначный селектор.
o.Billing.City = "Казань"
o.Shipping.City = "Самара"
Поле, объявленное прямо в Order, имело бы приоритет перед полями встроенных типов. Но когда приходится постоянно разбирать такие конфликты, именованные поля обычно читаются проще.
Встроить можно и указатель *Address. Тогда нулевое значение внешней структуры содержит nil в этом поле. Обращение к продвинутому полю c.City потребует разыменования и вызовет панику, пока указатель не инициализирован. Встраивание указателя не создаёт объект автоматически.
Выбирайте встраивание, когда хотите открыть поля или методы вложенного типа через внешний. Если адрес нужен только как часть данных клиента, запись Address Address явно показывает эту связь и не добавляет сокращённых селекторов.
Сравнение структур через ==
Значения одного типа структуры можно сравнивать через == и !=, если сравнимы все поля. Наш User содержит int, string и bool, поэтому сравнение разрешено:
a := User{ID: 42, Name: "Анна"}
b := User{ID: 42, Name: "Анна"}
c := User{ID: 42, Name: "Борис"}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
Сравниваются соответствующие поля. Совпадения одного ID недостаточно: разные имена делают структуры неравными.
Сравнимость проверяется и для вложенных полей. Если поле содержит другую структуру, её поля тоже должны быть сравнимы. Массив сравним, если сравним его тип элементов. Срезы, карты и функции несравнимы между собой, хотя каждое такое значение можно сравнить с nil.
Поэтому наличие среза запрещает == для всей структуры:
type Group struct {
Name string
Members []string
}
// Внутри main:
a := Group{Name: "API"}
b := Group{Name: "API"}
// fmt.Println(a == b) // Ошибка компиляции, даже если оба среза nil.
fmt.Println(a.Members == nil, b.Members == nil) // true true
То же ограничение действует для ключей карты. Структура со сравнимыми полями подходит для составного ключа:
type CacheKey struct {
UserID int
Locale string
}
// Внутри main:
cache := map[CacheKey]string{
{UserID: 42, Locale: "ru"}: "Привет",
}
fmt.Println(cache[CacheKey{UserID: 42, Locale: "ru"}]) // Привет
Если добавить в CacheKey поле со срезом, тип перестанет подходить для ключа. Подробнее о картах читайте в статье map в Go.
Поле any: компилируется, но может вызвать панику
Интерфейсные типы сравнимы, однако результат зависит от значений внутри них:
type Box struct {
Value any
}
// Внутри main:
a := Box{Value: []int{1, 2}}
b := Box{Value: []int{1, 2}}
// fmt.Println(a == b) // Паника: внутри интерфейсов несравнимые срезы.
fmt.Println(a.Value == nil) // false
Компилятор разрешает a == b, потому что поле any имеет интерфейсный тип. Во время выполнения сравнение доходит до двух значений одного несравнимого динамического типа []int и вызывает панику.
Поэтому структура с any требует проверки того, что вы туда кладёте. Сам факт успешной компиляции не гарантирует безопасного сравнения всех её значений.
Сравнение указателей и значений
Для указателей == проверяет совпадение адресов, а не содержимого полей:
a := &User{ID: 42}
b := &User{ID: 42}
fmt.Println(a == b) // false
fmt.Println(*a == *b) // true
Это два отдельных объекта с одинаковыми полями. Перед разыменованием указателей нужно убедиться, что они не равны nil. Для указателей на объекты нулевого размера есть отдельная оговорка спецификации: они могут оказаться как равными, так и неравными. Для показанного User она не применяется.
Как сравнивать структуры со срезами
Если == недоступен, сначала определите смысл равенства. Для команды он может включать имя и участников в том же порядке. Тогда напишите функцию с явными правилами, используя пакет slices:
func EqualTeams(a, b Team) bool {
return a.Name == b.Name && slices.Equal(a.Members, b.Members)
}
slices.Equal сравнивает длину и соответствующие элементы. Для него пустой срез и nil равны. Порядок имеет значение: {"Анна", "Борис"} и {"Борис", "Анна"} дадут false.
Если порядок участников не важен, этой функции недостаточно. Нужно отдельно определить, допустимы ли повторы и должны ли одинаковые имена обозначать одного человека. Это правило предметной области, которое библиотека не выберет за вас.
В тестах встречается reflect.DeepEqual. Он рекурсивно сравнивает значения, но у него другие правила для пустых срезов:
var a []string
b := []string{}
fmt.Println(slices.Equal(a, b)) // true
fmt.Println(reflect.DeepEqual(a, b)) // false
Для примера нужны импорты slices и reflect. Используйте DeepEqual, только если его правила соответствуют проверке. Когда равенство определяет поведение приложения, явная функция позволяет сразу увидеть, какие поля участвуют в сравнении.
Теги полей: пример с JSON
В объявлении структуры после типа поля можно указать тег. Например, пакет encoding/json читает теги json:
type Profile struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
// Внутри main:
data, err := json.Marshal(Profile{Name: "Анна"})
if err != nil {
panic(err)
}
fmt.Println(string(data)) // {"name":"Анна"}
Здесь нужен импорт encoding/json. Поле Name превращается в ключ name. Поле Email пропущено: для строки опция omitempty исключает поле с пустым значением.
Тег не переименовывает Go-поле: в коде оно по-прежнему называется Name. Сам по себе тег не проверяет данные и не меняет присваивание. Его читает конкретный пакет или ваш код через reflect.
Для обычного строкового поля, объявленного с маленькой буквы, одного тега недостаточно: encoding/json его не сериализует. Чтобы исключить экспортируемое поле из JSON, используйте json:"-".
Не используйте теги как замену проверке входных данных. Если имя обязательно, проверяйте Name после разбора запроса или в функции создания объекта.
Итог
Начинайте с литералов с именами полей: User{Name: "Анна"}. Они показывают, что именно вы заполняете, и позволяют пропустить поля с подходящими нулевыми значениями.
При копировании структуры проверяйте поля со срезами, картами и указателями. Их копии могут обращаться к общим данным. Для изменения самой структуры через метод используйте указатель в получателе.
Встраивание открывает сокращённый доступ к вложенным полям и методам. Если такой доступ не нужен, именованное поле проще читать. Перед сравнением через == проверьте типы всех полей, а для структуры со срезами задайте правила равенства явно.
Правила языка описаны в спецификации: структуры, селекторы, наборы методов и сравнение. Поведение библиотечных функций можно сверить с документацией slices.Equal, reflect.DeepEqual и json.Marshal.
Теги: