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

Структури

Структура (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  — явний шлях теж працює

Вбудовування — це механізм композиції в 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" в зворотних лапках; кілька ключів розділяються пробілами.

Порожня структура 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{}{} значення порожньої структури на нуль байтів

Джерела