Перейти до змісту

Структури

Структура (struct) — це типізований набір іменованих полів, склеєних в одне значення. Це основний інструмент Go для моделювання «речі з кількома атрибутами» — точки, користувача, HTTP-запиту. Немає ні класів, ні успадкування, ні ключового слова для конструктора: структура — це просто дані, розкладені в пам'яті, а поведінку навколо них ви будуєте окремо (за допомогою функцій та методів).

Майже завжди структурі дають ім'я через ключове слово type з власних типів:

type Point struct {
    X int
    Y int
}

Це оголошує новий тип Point, значення якого несуть два поля типу int, X та Y. Поля одного типу можна записати в один рядок:

type Point struct {
    X, Y int
}

Створення значень структури

Створити Point можна кількома способами, і різниця між ними має значення.

Літерал з ключами — називаємо поля. Саме цю форму варто використовувати майже завжди: вона не залежить від порядку й переживе додавання нового поля кимось пізніше.

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

Позиційний літерал — значення в порядку оголошення полів, без імен. Крихкий: ламається, щойно структура отримає нове поле або поля переставлять місцями, і він вимагає значення для кожного поля.

p := Point{1, 2}      // ок, але прив'язаний до порядку оголошення

Нульове значення — оголошуємо без ініціалізації, і кожне поле отримує нульове значення свого типу (0, "", nil, false, …). У структури немає окремого «неініціалізованого» стану; нульова структура — це повноцінне, придатне до використання значення.

var p Point
fmt.Println(p)        // output: {0 0}

У літералі з ключами можна пропускати поля; ті, що ви опустили, набувають нульового значення:

p := Point{Y: 5}
fmt.Println(p)        // output: {0 5}

З погляду Python: немає __init__. Нульове значення і є вашим конструктором за замовчуванням. Коли нуль не є розумним значенням за замовчуванням, за домовленістю використовують звичайну функцію з ім'ям NewPoint(...) Point — звичайну функцію, а не спеціальний синтаксис.

Читання та запис полів

Крапкова нотація, і поля є адресовними, тож їм можна присвоювати значення напряму:

p := Point{X: 1, Y: 2}
p.X = 10
fmt.Println(p.X + p.Y)   // output: 12

Структури — це типи-значення

Присвоєння структури, передавання її у функцію чи повернення з функції копіює кожне поле. Копія незалежна від оригіналу.

a := Point{X: 1, Y: 2}
b := a            // повна копія
b.X = 99
fmt.Println(a.X)  // output: 1  — a лишається незмінною

Це найважливіше, що потрібно засвоїти. Якщо ви хочете, щоб функція змінювала структуру викликача, передавайте вказівник:

func moveRight(p *Point) {
    p.X++         // p.X — це скорочення для (*p).X — Go розіменовує автоматично
}

a := Point{X: 1, Y: 2}
moveRight(&a)
fmt.Println(a.X)  // output: 2

Зверніть увагу на p.X для *Point: Go автоматично розіменовує вказівник на структуру для доступу до поля, тож вам ніколи не доводиться писати (*p).X. Базове правило дивіться у вказівниках.

Порівняння структур

Структуру можна порівнювати через ==, якщо всі її поля є порівнюваними. Порівняння відбувається поле за полем.

p := Point{1, 2}
q := Point{1, 2}
fmt.Println(p == q)   // output: true

Це також робить порівнювані структури придатними для використання як ключі map. Але якщо структура містить непорівнюване поле — slice, map чи функцію — вся структура стає непорівнюваною, і == є помилкою компіляції:

type Bag struct {
    items []int
}
b1 := Bag{}
b2 := Bag{}
_ = b1 == b2          // compile error: struct containing []int cannot be compared

Вкладені та вбудовані структури

Поле саме може бути структурою:

type Line struct {
    Start Point
    End   Point
}

l := Line{
    Start: Point{0, 0},
    End:   Point{3, 4},
}
fmt.Println(l.End.Y)   // output: 4

Якщо оголосити поле без імені — лише тип — таке поле є вбудованим (embedded), а його поля підвищуються (promoted), тож до них можна звертатися напряму:

type Circle struct {
    Point      // вбудоване: без імені поля, лише тип
    Radius int
}

c := Circle{Point: Point{X: 1, Y: 2}, Radius: 5}
fmt.Println(c.X)        // output: 1  — підвищене з вбудованого Point
fmt.Println(c.Point.Y)  // output: 2  — явний шлях теж працює

Підвищені поля як ключі літерала

Ключем у складеному літералі може бути будь-який чинний селектор поля для цього типу структури, а не лише ім'я поля верхнього рівня. Тож підвищені поля працюють як ключі напряму, і вбудовану структуру можна заповнити, не називаючи її:

type Base struct{ ID int }
type Meta struct{ Tag string }

type User struct {
    Base
    Meta
    Name string
}

u := User{ID: 7, Tag: "admin", Name: "ada"}
fmt.Printf("%+v\n", u)
// output: {Base:{ID:7} Meta:{Tag:admin} Name:ada}

Підвищення сягає крізь стільки рівнів, скільки потрібно, тож поле, вбудоване на два рівні вглиб, теж придатне як ключ:

type Inner struct{ Deep int }
type Mid struct{ Inner }
type Outer struct {
    Mid
    Name string
}

o := Outer{Deep: 5, Name: "x"}
fmt.Println(o.Deep)   // output: 5

Два правила бережуть однозначність. Не можна в одному літералі задати і вбудоване поле, і одне з його підвищених полів — вони змагалися б за ту саму пам'ять:

o := Outer{Mid: Mid{}, Deep: 2}
// compile error: cannot specify promoted field Deep and enclosing embedded field Mid

А ім'я, підвищене з двох вбудованих типів на однаковій глибині, узагалі не є чинним селектором — отже, не є й чинним ключем:

type A struct{ X int }
type B struct{ X int }
type C struct {
    A
    B
}

c := C{X: 1}   // compile error: unknown field X in struct literal of type C

Вбудовування — це механізм композиції в Go; він заступає той бік успадкування, що стосується даних, у інших мовах. Бік методів у вбудовуванні (підвищення методів) розглядається в методах.

Анонімні структури

Можна створити значення структури, так і не оголосивши іменований тип. Зручно для одноразового групування — рядка таблиці тестів, швидкої форми JSON — де type верхнього рівня був би зайвим шумом.

config := struct {
    Host string
    Port int
}{
    Host: "localhost",
    Port: 8080,
}
fmt.Println(config.Host, config.Port)   // output: localhost 8080

Теги структур

Кожне поле може нести тег: сирий рядковий літерал після типу. Теги — це метадані: компілятор їх ігнорує, але бібліотеки читають їх під час виконання через рефлексію. Канонічне застосування — керування тим, як encoding/json називає поля:

type User struct {
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
}

u := User{Name: "Ada"}
b, _ := json.Marshal(u)
fmt.Println(string(b))   // output: {"name":"Ada"}

Тут Email відкидається через omitempty і його нульове (порожнє) значення. Без тегів ключами були б "Name" та "Email" — імена полів Go. Теги за домовленістю — це пари key:"value" в зворотних лапках; кілька ключів розділяються пробілами.

Тег — це просто рядок

Для компілятора в тезі немає нічого особливого. Це один рядок, що зберігається поряд із полем, і будь-який пакет може запитати в нього ключ:

type Item struct {
    Name string `json:"name" xml:"item-name"`
}

f, _ := reflect.TypeOf(Item{}).FieldByName("Name")
fmt.Printf("%q %q %q\n", f.Tag.Get("json"), f.Tag.Get("xml"), f.Tag.Get("nope"))
// output: "name" "item-name" ""

Невідомий ключ повертається порожнім, тож дві бібліотеки можуть читати те саме поле, нічого не знаючи одна про одну. encoding/json читає json:, encoding/xml читає xml:, і жодна з них не помічає іншу:

j, _ := json.Marshal(Item{Name: "bolt"})
fmt.Println(string(j))   // output: {"name":"bolt"}

x, _ := xml.Marshal(Item{Name: "bolt"})
fmt.Println(string(x))   // output: <Item><item-name>bolt</item-name></Item>

Зворотний бік у тому, що описка в тезі мовчазна. jsom:"name" спокійно компілюється, і encoding/json просто відкочується до імені поля:

type Typo struct {
    Name string `jsom:"name"`
}

b, _ := json.Marshal(Typo{Name: "bolt"})
fmt.Println(string(b))   // output: {"Name":"bolt"}

- пропускає поле, а теги переживають вбудовування

Значення тегу - каже бібліотеці, що читає, взагалі відкинути поле. Це відрізняється від omitempty, яка відкидає поле лише тоді, коли воно має своє нульове значення. Неекспортовані поля ніколи не серіалізуються взагалі, з тегом чи без нього. А коли структуру вбудовано, її поля підвищуються разом зі своїми тегами:

type Point struct {
    X int `json:"x"`
    Y int `json:"y"`
}

type Shape struct {
    Point                     // X та Y підвищуються разом з тегами
    Label  string `json:"label"`
    secret string             // неекспортоване: ніколи не серіалізується
    Cache  []byte `json:"-"`  // експортоване, але явно пропущене
}

s := Shape{Point: Point{X: 1, Y: 2}, Label: "corner"}
b, _ := json.Marshal(s)
fmt.Println(string(b))   // output: {"x":1,"y":2,"label":"corner"}

x та y опиняються на верхньому рівні виводу, а не вкладеними під ключ Point — підвищення тут таке саме пласке, як і для доступу до полів.

З погляду Python: тут не працює жоден декоратор. Тег — це інертні дані, і саме бібліотека, якій ви передаєте структуру, надає їм сенсу — тому одна структура може одночасно описувати і свою JSON-форму, і свою XML-форму.

Порожня структура struct{}

Структура без полів займає нуль байтів. Вона не несе жодних даних — її використовують суто як сигнал. Два поширені застосування — це множина (map, значення якої вас не цікавлять) та канал, що сигналізує «подія сталася», не надсилаючи корисного навантаження:

seen := map[string]struct{}{}
seen["go"] = struct{}{}
_, ok := seen["go"]
fmt.Println(ok)          // output: true

struct{}{} спершу читається дивно: внутрішнє struct{} — це тип (порожня структура), зовнішнє {} — це літерал (значення цього типу).

Швидка довідка

Форма Значення
type T struct { X, Y int } оголосити іменований тип-структуру
T{X: 1, Y: 2} літерал з ключами (бажано)
T{1, 2} позиційний літерал (прив'язаний до порядку)
var t T нульове значення — усі поля обнулені
t.X доступ до поля (авто-розіменування через *T)
a == b поле за полем, лише якщо всі поля порівнювані
вбудоване поле (тип без імені) підвищує внутрішні поля
`json:"name"` тег поля, який бібліотеки читають через рефлексію
struct{}{} значення порожньої структури на нуль байтів

Джерела