Skip to content

Структуры в Go

Структура может хранить набор данных различных типов и является составным типом. Go отказался от классов и наследования, а также от конструкторов, намеренно ослабив возможности ООП. Go не является традиционным ООП-языком, но в нём есть элементы ООП — через структуры и методы можно имитировать класс. Ниже приведён простой пример структуры:

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

Объявление

Объявление структуры очень простое:

go
type Person struct {
   name string
   age int
}

Сама структура и её поля следуют правилам именования с учётом регистра. Для соседних полей одного типа можно указать тип один раз:

go
type Rectangle struct {
  height, width, area int
  color               string
}

TIP

При объявлении полей структуры имена полей не должны совпадать с именами методов.

Инициализация

В Go нет конструкторов. В большинстве случаев структура инициализируется следующим образом — при инициализации указываются имена полей, как в map:

go
programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

Можно опустить имена полей, но тогда необходимо инициализировать все поля. Обычно такой способ не рекомендуется из-за плохой читаемости:

go
programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

Если процесс инициализации сложен, можно написать функцию для инициализации структуры — своего рода конструктор:

go
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 подход, позволяющий гибко инициализировать структуры с высокой расширяемостью без изменения сигнатуры конструктора. Предположим, есть такая структура:

go
type Person struct {
  Name     string
  Age      int
  Address  string
  Salary   float64
  Birthday string
}

Объявим тип PersonOptions, принимающий параметр *Person. Он должен быть указателем, поскольку в замыкании мы будем присваивать значения полям Person:

go
type PersonOptions func(p *Person)

Далее создадим функции options, обычно начинающиеся с With. Их возвращаемое значение — замыкание:

go
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:

go
func NewPerson(options ...PersonOptions) *Person {
    // Сначала применяем options
  p := &Person{}
    for _, option := range options {
        option(p)
    }

  // Обработка значений по умолчанию
  if p.Age < 0 {
    p.Age = 0
  }
  ......

    return p
}

Теперь для различных требований инициализации достаточно одного конструктора — нужно лишь передать разные функции Options:

go
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 отношения между структурами выражаются через композицию — явную или анонимную. Последняя напоминает наследование, но по сути ничем не отличается. Например:

Явная композиция:

go
type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

При использовании необходимо явно указывать поле p:

go
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)

Анонимная композиция не требует явного указания поля:

go
type Person struct {
  name string
  age  int
}

type Student struct {
  Person
  school string
}

type Employee struct {
  Person
  job string
}

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

go
student := Student{
   Person: Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.name)

Указатели

Для указателя на структуру не требуется явное разыменование — можно напрямую обращаться к содержимому:

go
p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age, p.name)

При компиляции это преобразуется в (*p).name, (*p).age. Фактически разыменование происходит, но синтаксис позволяет его опустить — это своего рода синтаксический сахар.

Теги

Теги структур — форма метапрограммирования. В сочетании с отражением они позволяют создавать удивительные возможности. Формат:

go
`key1:"val1" key2:"val2"`

Теги представляют собой пары ключ-значение, разделённые пробелами.容错性很低 — если формат тега неверен, прочитать его не удастся, хотя при компиляции ошибок не возникает. Пример использования:

go
type Programmer struct {
    Name     string `json:"name"`
    Age      int `yaml:"age"`
    Job      string `toml:"job"`
    Language []string `properties:"language"`
}

Теги структур чаще всего используются для определения псевдонимов при сериализации. Для полноценной работы тегов необходимо отражение.

Выравнивание памяти

Расположение полей структуры в памяти следует правилам выравнивания. Это уменьшает количество обращений процессора к памяти, но увеличивает занимаемую память — компромисс между пространством и временем. Предположим, есть структура:

go
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 байт бесполезны.

Рассмотрим другую структуру:

go
type Num struct {
  A int8
  B int64
  C int8
}

Согласно правилам, она также занимает 24 байта, хотя имеет только три поля — 14 байт теряются впустую.

Можно изменить порядок полей:

go
type Num struct {
  A int8
  C int8
  B int64
}

Теперь структура занимает 16 байт, теряется 6 байт — экономия 8 байт.

Теоретически разумный порядок полей уменьшает потребление памяти. Однако на практике это не всегда даёт существенную экономию, но повышает нагрузку на разработчика. Особенно если полей десятки или сотни. Поэтому достаточно просто знать об этом.

TIP

Если вы действительно хотите сэкономить память, обратите внимание на эти библиотеки:

Они анализируют структуры в коде и переставляют поля для минимизации занимаемой памяти.

Пустая структура

Пустая структура не имеет полей и не занимает памяти. Можно проверить размер через unsafe.Sizeof:

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.Sizeof(Empty{}))
}

Вывод:

0

Пустые структуры используются в различных сценариях: как тип значения map для имитации set, как тип канала для уведомления и т.д.

Golang by www.golangdev.cn edit