Структуры в Go
Структура может хранить набор данных различных типов и является составным типом. Go отказался от классов и наследования, а также от конструкторов, намеренно ослабив возможности ООП. Go не является традиционным ООП-языком, но в нём есть элементы ООП — через структуры и методы можно имитировать класс. Ниже приведён простой пример структуры:
type Programmer struct {
Name string
Age int
Job string
Language []string
}Объявление
Объявление структуры очень простое:
type Person struct {
name string
age int
}Сама структура и её поля следуют правилам именования с учётом регистра. Для соседних полей одного типа можно указать тип один раз:
type Rectangle struct {
height, width, area int
color string
}TIP
При объявлении полей структуры имена полей не должны совпадать с именами методов.
Инициализация
В Go нет конструкторов. В большинстве случаев структура инициализируется следующим образом — при инициализации указываются имена полей, как в map:
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}Можно опустить имена полей, но тогда необходимо инициализировать все поля. Обычно такой способ не рекомендуется из-за плохой читаемости:
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}Если процесс инициализации сложен, можно написать функцию для инициализации структуры — своего рода конструктор:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}Однако Go не поддерживает перегрузку функций и методов, поэтому нельзя определить одну и ту же функцию с разными параметрами. Для множественных способов инициализации следует создать несколько конструкторов или использовать паттерн options.
Паттерн Options
Паттерн options — распространённый в Go подход, позволяющий гибко инициализировать структуры с высокой расширяемостью без изменения сигнатуры конструктора. Предположим, есть такая структура:
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}Объявим тип PersonOptions, принимающий параметр *Person. Он должен быть указателем, поскольку в замыкании мы будем присваивать значения полям Person:
type PersonOptions func(p *Person)Далее создадим функции options, обычно начинающиеся с With. Их возвращаемое значение — замыкание:
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}Фактическая сигнатура конструктора выглядит так — он принимает переменное число параметров PersonOptions:
func NewPerson(options ...PersonOptions) *Person {
// Сначала применяем options
p := &Person{}
for _, option := range options {
option(p)
}
// Обработка значений по умолчанию
if p.Age < 0 {
p.Age = 0
}
......
return p
}Теперь для различных требований инициализации достаточно одного конструктора — нужно лишь передать разные функции Options:
func main() {
p1 := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}Паттерн функциональных options встречается во многих открытых проектах. Например, инициализация gRPC Server использует этот паттерн. Он подходит для сложных случаев инициализации; если параметров немного, лучше использовать обычный конструктор.
Композиция
В Go отношения между структурами выражаются через композицию — явную или анонимную. Последняя напоминает наследование, но по сути ничем не отличается. Например:
Явная композиция:
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}При использовании необходимо явно указывать поле p:
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)Анонимная композиция не требует явного указания поля:
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}Имя анонимного поля по умолчанию — имя типа. Вызывающий код может напрямую обращаться к полям и методам этого типа. Это удобнее, но иначе ничем не отличается от первого способа:
student := Student{
Person: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.name)Указатели
Для указателя на структуру не требуется явное разыменование — можно напрямую обращаться к содержимому:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age, p.name)При компиляции это преобразуется в (*p).name, (*p).age. Фактически разыменование происходит, но синтаксис позволяет его опустить — это своего рода синтаксический сахар.
Теги
Теги структур — форма метапрограммирования. В сочетании с отражением они позволяют создавать удивительные возможности. Формат:
`key1:"val1" key2:"val2"`Теги представляют собой пары ключ-значение, разделённые пробелами.容错性很低 — если формат тега неверен, прочитать его не удастся, хотя при компиляции ошибок не возникает. Пример использования:
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}Теги структур чаще всего используются для определения псевдонимов при сериализации. Для полноценной работы тегов необходимо отражение.
Выравнивание памяти
Расположение полей структуры в памяти следует правилам выравнивания. Это уменьшает количество обращений процессора к памяти, но увеличивает занимаемую память — компромисс между пространством и временем. Предположим, есть структура:
type Num struct {
A int64
B int32
C int16
D int8
E int32
}Известно, что эти типы занимают:
int64— 8 байтint32— 4 байтаint16— 2 байтаint8— 1 байт
Кажется, что структура занимает 8+4+2+1+4=19 байт. Но это не так. Согласно правилам выравнивания, размер структуры должен быть кратен размеру наибольшего поля. Наибольшее поле — int64 (8 байт), поэтому структура занимает 24 байта, из которых 5 байт бесполезны.

Рассмотрим другую структуру:
type Num struct {
A int8
B int64
C int8
}Согласно правилам, она также занимает 24 байта, хотя имеет только три поля — 14 байт теряются впустую.

Можно изменить порядок полей:
type Num struct {
A int8
C int8
B int64
}Теперь структура занимает 16 байт, теряется 6 байт — экономия 8 байт.

Теоретически разумный порядок полей уменьшает потребление памяти. Однако на практике это не всегда даёт существенную экономию, но повышает нагрузку на разработчика. Особенно если полей десятки или сотни. Поэтому достаточно просто знать об этом.
TIP
Если вы действительно хотите сэкономить память, обратите внимание на эти библиотеки:
Они анализируют структуры в коде и переставляют поля для минимизации занимаемой памяти.
Пустая структура
Пустая структура не имеет полей и не занимает памяти. Можно проверить размер через unsafe.Sizeof:
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}Вывод:
0Пустые структуры используются в различных сценариях: как тип значения map для имитации set, как тип канала для уведомления и т.д.
