Перейти к содержимому
Теория · Структуры и методы · урок 1 из 11

Структуры

Чему вы научитесь

Чему вы научитесь:

  • объявлять свой тип-структуру и работать с его полями;
  • добавлять типу методы и выбирать между получателем-значением и получателем-указателем;
  • писать конструктор и решать, какие имена видны другим пакетам;
  • встраивать один тип в другой и отличать встраивание от наследования.
Анкета Point на планшете с двумя графами: X: 1 и Y: 2

Представьте бумажную анкету. В ней есть графы «Имя», «Возраст», «Город», и у каждой графы своя подпись. Всё, что вы о себе написали, лежит на одном листе. Анкету можно отдать целиком, а не диктовать каждую графу по отдельности.

Без такой анкеты связанные значения расползаются по разным переменным. Точка на плоскости — это два числа, и их можно хранить в двух переменных x и y. Но тогда возникают неудобства:

  • функция, которая принимает точку, берёт два параметра;
  • список точек превращается в два среза, которые нужно держать в согласии.

В Go связанные значения собирают в один тип:

GO
package main

import "fmt"

type Point struct {
    X int
    Y int
}

func main() {
    p := Point{X: 1, Y: 2}
    p.X = 10
    fmt.Println(p, p.X, p.Y) // {10 2} 10 2
}

Давайте разберём пример по строкам:

  1. type Point struct описывает бланк анкеты с графами X и Y.
  2. p := Point{X: 1, Y: 2} заполняет такую анкету: в X пишет 1, в Y — 2.
  3. p.X = 10 исправляет графу X.
  4. fmt.Println(p, p.X, p.Y) печатает всю структуру как {10 2}, а затем каждое поле отдельно.

Объявление типа

Структура — тип, который хранит несколько значений под одним именем. Каждое такое значение — поле со своим именем и типом. Объявление type Point struct { … } состоит из частей:

  • type — ключевое слово: «объявляю новый тип»;
  • Point — имя типа;
  • struct — вид типа: набор полей;
  • X int и Y int в фигурных скобках — поля, по одному в строке: имя, затем тип.

Поля одного типа можно перечислить через запятую: struct{ X, Y int }. К полю обращаются через точку: p.X читает его, а p.X = 10 записывает.

Здесь картинка с анкетой расходится с Go. На бумаге можно дописать графу на полях, а у структуры набор полей раз и навсегда задаёт её тип.

Литерал структуры

Значение структуры создают одним из трёх способов. Давайте разберём каждый.

Литерал с именами полей

Запись Point{X: 1, Y: 2} называет каждое поле и его значение через двоеточие. Порядок полей в такой записи не важен.

Поле, которого нет в литерале, получает нулевое значение — как пустая графа анкеты. Так, Point{X: 1} печатается как {1 0}.

Эту форму пишут чаще всего. Она понятнее и не ломается, когда в тип добавляют новое поле.

Литерал без имён

Запись Point{1, 2} перечисляет значения без имён полей. Она тоже компилируется, но у неё два ограничения:

  • нужны все поля, и строго в порядке объявления;
  • если пропустить поле, Point{1} не скомпилируется: too few values in struct literal of type Point.

Когда в тип добавляют новое поле, все такие литералы перестают компилироваться. Поэтому форму без имён оставляют для маленьких типов, которые не меняются.

Объявление через var

Запись var p Point создаёт структуру с нулевыми значениями во всех полях. Такая p печатается как {0 0} — это чистый бланк анкеты.

Присваивание копирует

Структура — такое же значение, как int или массив. Присваивание q := p копирует все поля, и дальше у q своя память — как ксерокопия анкеты, на которой можно писать, не трогая оригинал:

GO
p := Point{X: 1, Y: 2}
q := p
q.X = 9
fmt.Println(p.X, q.X) // 1 9

Разберём по строкам:

  1. p := Point{X: 1, Y: 2} создаёт оригинал.
  2. q := p снимает с него копию всех полей.
  3. q.X = 9 пишет только в копию.
  4. fmt.Println(p.X, q.X) печатает 1 9: оригинал не изменился.

Структуру, переданную в функцию, Go копирует так же: параметр получает свою копию всех полей.

Сравнение структур

Этот вопрос часто задают на собеседовании: «Можно ли сравнить две структуры?» Ответ состоит из трёх частей:

  • можно, оператором ==, если сравнимы все поля: он сравнивает их по порядку;
  • сразу после q := p выражение p == q даёт true;
  • если в структуре есть поле-срез или поле map, сравнение не компилируется: struct containing []string cannot be compared.

Поле через указатель

Чтобы изменить p через другую переменную, нужен указатель на неё. В теме об указателях это была бумажка с адресом дома — здесь по ней приходят к самой анкете.

Писать (*r).X не обязательно: для указателя на структуру Go сам подставляет разыменование. Запись r.X обращается к полю той структуры, на которую указывает r.

GO
p := Point{X: 1, Y: 2}
r := &p
r.X = 9
fmt.Println(p.X, r.Y) // 9 2

Разберём по строкам:

  1. r := &p кладёт в r адрес p.
  2. r.X = 9 меняет поле X в самой p.
  3. fmt.Println(p.X, r.Y) печатает 9 2: p.X уже изменено, а r.Y читает поле Y той же p.

Частая ошибка

Поля элементов среза меняют через переменную цикла range:

GO
pts := []Point{{1, 2}, {3, 4}}
for _, p := range pts {
    p.X = 0 // ✗
}
fmt.Println(pts) // [{1 2} {3 4}]

Программа компилируется, но срез не меняется: p получает копию очередного элемента, и запись уходит в эту копию. Чтобы изменить сам элемент, обращайтесь к нему по индексу: pts[i].X = 0 в цикле for i := range pts. Внутри литерала среза имя типа Point у элементов можно не писать.

Главное

  • Структура собирает поля с именами и типами в один тип; к полю обращаются через точку.
  • Присваивание и передача в функцию копируют все поля структуры.
  • Структуры сравнивают через ==, только если сравнимы все их поля.
  • Через указатель на структуру поле читают и меняют как r.X, без (*r).X.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом