Структури¶
Структура (struct) — це типізований набір іменованих полів, склеєних в одне значення. Це основний інструмент Go для моделювання «речі з кількома атрибутами» — точки, користувача, HTTP-запиту. Немає ні класів, ні успадкування, ні ключового слова для конструктора: структура — це просто дані, розкладені в пам'яті, а поведінку навколо них ви будуєте окремо (за допомогою функцій та методів).
Майже завжди структурі дають ім'я через ключове слово type з
власних типів:
Це оголошує новий тип Point, значення якого несуть два поля типу int,
X та Y. Поля одного типу можна записати в один рядок:
Створення значень структури¶
Створити Point можна кількома способами, і різниця між ними має
значення.
Літерал з ключами — називаємо поля. Саме цю форму варто використовувати майже завжди: вона не залежить від порядку й переживе додавання нового поля кимось пізніше.
Позиційний літерал — значення в порядку оголошення полів, без імен. Крихкий: ламається, щойно структура отримає нове поле або поля переставлять місцями, і він вимагає значення для кожного поля.
Нульове значення — оголошуємо без ініціалізації, і кожне поле отримує
нульове значення свого типу (0, "", nil, false, …). У структури
немає окремого «неініціалізованого» стану; нульова структура — це
повноцінне, придатне до використання значення.
У літералі з ключами можна пропускати поля; ті, що ви опустили, набувають нульового значення:
З погляду Python: немає
__init__. Нульове значення і є вашим конструктором за замовчуванням. Коли нуль не є розумним значенням за замовчуванням, за домовленістю використовують звичайну функцію з ім'ямNewPoint(...) Point— звичайну функцію, а не спеціальний синтаксис.
Читання та запис полів¶
Крапкова нотація, і поля є адресовними, тож їм можна присвоювати значення напряму:
Структури — це типи-значення¶
Присвоєння структури, передавання її у функцію чи повернення з функції копіює кожне поле. Копія незалежна від оригіналу.
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. Базове правило дивіться у вказівниках.
Порівняння структур¶
Структуру можна порівнювати через ==, якщо всі її поля є
порівнюваними. Порівняння відбувається поле за полем.
Це також робить порівнювані структури придатними для використання як
ключі 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{}{} |
значення порожньої структури на нуль байтів |
Джерела¶
- Struct types — go.dev/ref/spec#Struct_types
- Composite literals — go.dev/ref/spec#Composite_literals
- Comparison operators — go.dev/ref/spec#Comparison_operators
- Struct tags — pkg.go.dev/reflect#StructTag
- encoding/json#Marshal — pkg.go.dev/encoding/json#Marshal
- encoding/xml#Marshal — pkg.go.dev/encoding/xml#Marshal
- Effective Go: embedding — go.dev/doc/effective_go#embedding
- Selectors — go.dev/ref/spec#Selectors