---
title: "Структуры"
description: "Структуры в go: type struct, поля, композитные литералы, доступ p.Field, нулевая структура, вложенность и embedding, сравнение ==, карты структур, %+v."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/23-struktury/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 22
---

# Структуры

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 23 из 25. ~22 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/23-struktury/*

Структуры Go — первый инструмент языка для изобретения собственных типов: несколько полей разных типов, собранных под одним именем, вместо россыпи несвязанных переменных. В [уроке об указателях](/courses/go-basics/lessons/22-ukazateli/) тип Player уже мелькал как декорация — теперь разберём его всерьёз: объявление type и поля, композитные литералы двух видов и почему именованные считаются идиоматичными, нулевая структура, вложенность и встраивание, сравнение на равенство и карты структур из урока о картах. Каждый ранимый пример прогнан на обоих движках песочницы, а расхождения честно оговорены.

### Структуры в Go: свой тип из полей

До этого момента вы пользовались чужими типами: int, string, bool, срезами и картами. Это готовая мебель, но у неё предел: карта из [урока о картах](/courses/go-basics/lessons/19-karty/) хранит пары «ключ — значение», однако не говорит, ЧТО именно лежит внутри и какие ключи обязательны. Игрок — это имя, счёт и уровень; заказ — номер, сумма и дата. Держать такие наборы в трёх отдельных переменных значит каждый раз передавать их по три штуки и надеяться, что не перепутаешь порядок. Структуры Go решают это ровно: вы объявляете собственный тип, в котором перечислены поля — имя каждого поля и его тип, — и дальше значения такого типа живут и передаются как одно целое.

Ключевое слово type вводит новое имя типа, а слово struct открывает список полей. После объявления Player становится полноправным типом: у него можно требовать переменные, срезы, карты, параметры функций — всё то же, что у int. При этом структура — это не «класс с данными» из других языков: в ней пока нет методов (им посвящён следующий урок), нет наследования, нет конструкторов — только честно сгруппированные данные, которые копируются, сравниваются и печатаются по понятным правилам.

В англоязычных материалах — типичный запрос struct golang — и почти каждый такой текст начинается с того же определения: struct is a collection of fields. Собрание полей — и это точнее, чем кажется: поля лежат вместе, но каждое сохраняет собственный тип и собственное имя, по которому к нему обращаются. Именно имя поля делает код читаемым: pl.Score говорит о счёте, тогда как pl[1] заставлял бы вспоминать, что мы там положили вторым.

В этом уроке: объявление типа и полей, композитный литерал в двух видах — именованный и позиционный, доступ к полям через точку (включая разыменование указателя, которое вы уже видели в 22-м), нулевая структура, вложенные структуры и встраивание, сравнение на равенство, карты структур и честный разговор о том, когда выбрать структуру, а когда карту.

### Песочница: первая структура

Запустите: type Card struct {...} объявляет новый тип с двумя строковыми полями. Строка a := Card{Rank: "7", Suit: "черви"} создаёт значение именованным литералом — каждое поле названо по имени; печать a.Rank, a.Suit показывает доступ через точку. Печать самой структуры fmt.Println(a) даёт {7 черви}: фигурные скобки и значения полей по порядку объявления. Затем b := Card{"8", "пики"} — позиционный литерал: значения перечислены по порядку полей, без имён. Опыты. Первый: создайте c := Card{Rank: «дама»} — без масти; печать покажет {дама }: пропущенное поле получило нулевое значение типа, для строки это пустая строка. Второй: поменяйте местами значения в позиционном литерале Card{«пики», «8»} — напечатается «пики 8»: позиционный литерал привязан к ПОРЯДКУ полей, а не к смыслу, и язык не заметит подвоха. Третий: попробуйте b.Rank = «король» и напечатайте b — поля правятся так же свободно, как обычные переменные.

```go
package main

import "fmt"

// Card — игральная карта: номинал и масть
type Card struct {
        Rank string
        Suit string
}

func main() {
        a := Card{Rank: "7", Suit: "черви"}
        fmt.Println(a.Rank, a.Suit)
        fmt.Println(a)
        b := Card{"8", "пики"}
        fmt.Println(b.Rank, b.Suit)
}
```

### Объявление типа: поля, порядок, групповая форма

Разберём анатомию объявления. type Card struct { — начало нового типа с именем Card; дальше построчно перечислены поля: имя поля, затем его тип. Закрывающая фигурная скобка завершает список. Имена полей внутри одного типа не повторяются; типы могут быть любыми — числами, строками, срезами, картами и другими структурами. Порядок полей имеет значение ровно в двух случаях: для позиционного литерала (значения заполняются по порядку) и для раскладки в памяти; на именованный литерал порядок не влияет.

Если несколько полей подряд имеют один тип, их можно перечислить через запятую, назвав тип один раз: Name, City string объявит сразу два строковых поля. Это групповая форма — экономия вертикали, ничего больше; в ранимых примерах курса она уже встречалась у параметров функций и работает точно так же. Полный синтаксис с точкой с запятой между полями тоже легален (в одной строке можно объявить несколько полей через точку с запятой), но в идиоматичном Go поля пишут с новой строки, а точки с запятой вставляет сам компилятор.

Имена полей подчиняются тому же правилу заглавной буквы, что и имена функций: поле с заглавной буквы — Name — экспортируется, то есть видно из других пакетов; поле со строчной — name — приватно для пакета. Пока вы пишете программы в одном файле, разницы нет, но раз встретили — знайте: это не стиль, а правило видимости, и оно работает для полей структур так же, как для всего остального.

Структура может не иметь полей вовсе: type Flag struct {} — пустая структура. Звучит как курьёз, но у неё есть настоящая работа — она занимает ноль байт и используется как «метка присутствия», например в картах-множествах: map[string]Flag хранит только факты «ключ был виден». Пробник проверил пустые структуры на обоих движках — они работают, но в основных примерах урока мы остаёмся на структурах с содержимым.

### Композитный литерал: именованный и позиционный

Литерал — это запись значения прямо в коде: 42 — литерал числа, "аня" — литерала строки. Для структур значение собрано из нескольких частей, поэтому запись называется композитный литерал — составной. Виды два, и выбор между ними — первое стилистическое решение, которое вы принимаете в каждой строке со структурой.

Именованный литерал указывает поле при каждом значении: Player{Name: "аня", Score: 10}. Читается без заглядывания в объявление типа: видно, что имя — «аня», а счёт — 10. Поля можно перечислять в любом порядке, можно перечислить не все — пропущенные получат нулевые значения своих типов. Это не баг, а встроенная черта: Player{Name: "аня"} — честная структура со счётом 0. Именно за самодокументируемость и устойчивость к правкам именованный литерал считается идиоматичным: добавите в тип новое поле — весь существующий код продолжит компилироваться, а места, где поле действительно нужно, подсветит компилятор или вы сами при чтении.

Позиционный литерал перечисляет значения по порядку полей: Player{"аня", 10}. Короче на десяток символов, но требует от читателя помнить объявление типа, а от автора — дисциплины: перечислить нужно ВСЕ поля, ничего не пропустив, и ровно в порядке объявления. Пропустите значение — настоящий Go откажется собирать программу с ошибкой too few values in struct literal; перепутаете порядок строковых полей — компилятор промолчит, а масть окажется номиналом, как в опыте из песочницы выше.

Смешивать два стиля в одном литерале нельзя: Player{"аня", Score: 10} — ошибка компиляции mixture of field:value and value elements. Практическое правило простое: в своём коде пишите именованные — даже для двух полей; позиционные встречайте в чужом коде и коротких структурах вроде координат, где порядок очевиден, как в Point{3, 4}. В песочнице резервный движок мягче настоящего: он не ловит ни неполный позиционный, ни смесь — поэтому в ранимых примерах урока только полные литералы обоих видов, а о расхождениях — отдельная честная сводка в конце.

### Проверьте себя: какой литерал идиоматичен

**Вопрос:** Тип объявлен как type Order struct { ID int; Total int }. Команда пишет новый литерал. Какой вариант и почему считается идиоматичным?

- ✅ Order{ID: 7, Total: 900}: имена полей самодокументируют код, порядок не важен, можно опустить часть полей — они получат нули
- Order{7, 900}: позиционная форма короче, а значит всегда лучше
- Разницы нет: оба вида проверяют соответствие имён полей и значений одинаково строго
- Именованная форма запрещена при больше двух полях — для длинных структур только позиционная

Именованный композитный литерал — идиома языка: каждое значение подписано именем поля, поля можно переставлять и опускать (пропущенные получают нулевое значение), а добавление нового поля в тип не ломает существующие литералы. Позиционный — легален, но требует всех полей строго по порядку и заставляет читателя держать объявление типа в голове. Смешивать формы в одном литерале нельзя — настоящий Go не компилирует такой код.

### Доступ к полям: точка и мост из урока об указателях

Обращение к полю — точка: pl.Name читает, pl.Score = 42 пишет, pl.Score += 5 читает-считает-пишет одним движком выражения. Точка работает на любых полях любых типов: у строкового поля — строковые операции, у числового — арифметика, а у поля-структуры — дальше по цепочке, о чём чуть ниже.

В [уроке 22](/courses/go-basics/lessons/22-ukazateli/) вы уже трогали структуры через указатели: функция levelUp(p *Player) прибавляла к счёту через p.Score += 5, и правка была видна снаружи. Там остался недосказанный финальный штрих — почему p.Score работает без звёздочек. Строгое чтение такое: p — указатель, и прежде чем взять поле, надо разыменовать: (*p).Score. Go разрешает опустить скобки и звёздочку — компилятор вставляет разыменование сам; такая короткая форма называется синтаксическим сахаром: запись, которая разворачивается в полную автоматически. Одна форма на все случаи — никакого выбора между точкой и стрелкой, как в C, меньше поводов для ошибок.

Значит, p.Name и (*p).Name — синонимы, и пробник гонял их в обоих движках с одинаковым выводом. Когда сахар разворачивается в уме, чужой код перестаёт удивлять: видите p.Score += 5 у переменной с типом-указателем — мысленно читайте (*p).Score += 5. А в 22-м вы брали адрес и у поля: &pl.Score — указатель на одно поле; правило «поля структуры живут по стабильным адресам» остаётся в силе на весь этот урок.

Копирование — вторая половина механики. Присваивание b := a копирует ВСЮ структуру: два значения после этого независимы, правка b.X не трогает a — в отличие от срезов и карт, где копируется заголовок, а данные остаются общими. То же происходит при передаче структуры в функцию без амперсанда: функция правит копию. Это поведение называется семантикой значения, и оно делает структуры предсказуемыми: у структуры-значения нет скрытых общих данных, а нужен доступ к оригиналу — передавайте указатель, как вы уже умеете.

### Песочница: правка поля и указатель

Запустите: levelUp(&pl) получила адрес структуры, и p.Score += 5 внутри функции подняла счёт оригинала — первая печать 8. Затем p := &pl и правка имени через сахар: вторая печать показывает (*p).Name — ту же короткую форму, развёрнутую в полную, — и p.Score: «борис-победитель 8». Обратите внимание: (*p).Name и p.Name в этой программе дают одно и то же — сахар и полная форма взаимозаменяемы. Опыты. Первый: добавьте копию c := pl, поменяйте c.Score = 0 и напечатайте pl.Score — останется 8: копия независима. Второй: поменяйте в печати (*p).Name на p.Name — вывод не изменится; убедитесь сами, что это одна операция в двух записях. Третий: вызовите levelUp(&pl) ещё дважды — счёт накопится: 13, 18.

```go
package main

import "fmt"

// Player — игрок: имя и счёт
type Player struct {
        Name  string
        Score int
}

// levelUp поднимает счёт игрока на пять очков
func levelUp(p *Player) {
        p.Score += 5
}

func main() {
        pl := Player{Name: "борис", Score: 3}
        levelUp(&pl)
        fmt.Println(pl.Score)
        p := &pl
        p.Name = "борис-победитель"
        fmt.Println((*p).Name, p.Score)
}
```

### Нулевая структура: объявили — уже работает

У каждого типа в Go есть нулевое значение — то, что лежит в переменной до первого присваивания. Для структуры это нулевые значения всех полей сразу: числа — 0, строки — пустая строка, булевы — false, указатели — nil. Объявление var p Player уже даёт готовую к работе структуру: p.Name — это пустая строка, p.Score — это 0, печать p показывает { 0} — пара нулевых значений в фигурных скобках (пустая строка печатается как пустота между скобкой и нулём). Никакой неинициализированной памяти, никакого мусора — язык гарантирует чистый старт.

Тот же механизм работает и внутри литералов: именованный литерал с пропущенными полями — это нулевая структура с исключениями. Player{Name: «аня»} — имя заполнено, счёт нулевой. Это осознанный идиоматичный приём: «всё по нулям, кроме вот этих полей» читается без единого лишнего слова. Отсюда же следует практика проектирования: нулевое значение должно быть осмысленным состоянием вашей структуры — буфер с длиной 0, счётчик с нулём, флаг false. Тогда var b Buffer без присваивания — уже корректный объект.

Печать нулевой структуры fmt.Println(p) — самый дешёвый способ заглянуть внутрь: { 0} говорит «строка пуста, число ноль» без единой строки кода форматирования. Для точечной отладки у Println есть старший брат Printf с глаголами форматирования из урока про вывод: %v печатает значения полей, а %+v добавляет имена — до него дойдём отдельным блоком.

И о сравнении с nil: структура-значение nil не бывает — нулевая структура это не nil, а честные нули в каждом поле. nil в историях про структуры появляется только у указателей на структуру: var p *Player — указатель, который ни на что не смотрит, и его разыменование без проверки — падение, как вы пережили в 22-м. Само nil-значение вам уже знакомо по проверкам ошибок из [урока 21](/courses/go-basics/lessons/21-oshibki-i-vozvraty/): там каждая строка if err != nil спрашивала «пуста ли ошибка» — тот же механизм «пустой ссылки», применённый к другому типу. Нулевая структура безопасна всегда; нулевой указатель — под контролем проверки.

### Что выведет код: нули и равенство

```go
package main

import "fmt"

type Point struct {
        X int
        Y int
}

func main() {
        a := Point{X: 1}
        b := Point{1, 0}
        fmt.Println(a == b)
        fmt.Println(a.X, b.Y)
}
```

**Что выведет программа?**

- ✅ true, затем 1 0: именованный литерал дописал Y нулём, и структуры совпали
- false, затем 1 0: литералы разных видов всегда неравны
- true, затем 1 1: позиционный литерал начал заполнять поля с конца
- Программа не скомпилируется: два вида литерала нельзя смешивать в сравнении

a := Point{X: 1} — именованный литерал с пропуском: Y получил нулевое значение, получилось {1 0}. b := Point{1, 0} — полный позиционный: X=1, Y=0 — та же пара. Сравнение == сопоставляет поля попарно, и структуры равны: печать true. Дальше a.X — это 1, b.Y — это 0: печать «1 0». Вид литерала влияет только на запись, а не на значение: после создания обе структуры неотличимы. Пробник прогнал этот код на обоих движках — вывод одинаков.

### Вложенные структуры: поле как целая структура

Полю структуры можно дать любой тип — в том числе другую структуру. Так данные собираются в дерево: у человека есть домашний адрес, у адреса — город и индекс. Объявление ничем не отличается от обычного поля: Name string и Home Address стоят рядом, просто у второго поля тип — целая структура. Собирается значение вложенным литералом: Person{Name: "аня", Home: Address{City: "Москва", Zip: "101000"}} — внутренний литерал целиком записан на месте поля.

Доступ идёт по цепочке точек: p.Home.City — «у человека — адрес — у адреса — город». Правка работает на любом уровне: p.Home.City = "Казань" меняет город внутри той же структуры. Печать целиком раскрывает вложенность в фигурных скобках: {аня {Казань 101000}} — внешние скобки у Person, внутренние у Address. Копирование при присваивании тоже честное: скопируется вся глубина — все поля всех уровней, независимая копия целиком.

Выбор между «плоской» структурой с полями City string прямо внутри и вложенной Address — решение о модели данных, а не о синтаксисе. Вложенность берут, когда блок полей живёт собственной жизнью: адрес есть у человека и у филиала, и его удобно передавать, копировать и печатать как одно целое. Если же поля никогда не переезжают поодиночке, плоская структура проще — меньше точек в цепочках доступа.

Существует и второй способ собрать структуру внутри структуры — встраивание, embedding: поле без имени, при котором поля внутреннего типа становятся доступны напрямую, без промежуточной точки. Механика другая, читается иначе и окружена мифами о наследовании — разберём её отдельным блоком сразу после песочницы.

### Песочница: вложенная структура

Запустите: p собран вложенным литералом — Address{...} целиком записан на месте поля Home. Первая печать идёт по цепочке точек: p.Name, p.Home.City — «аня Москва». Правка p.Home.City = "Казань" меняет поле внутреннего уровня, и печать всей структуры показывает {аня {Казань 101000}}: две пары фигурных скобок — внешняя структура и вложенная. Опыты. Первый: добавьте q := p и поменяйте q.Home.Zip = "999999" — печать p.Home.Zip покажет 101000: копия скопировала вложенность целиком, правка копии не видна. Второй: соберите вторую позиционным литералом Person{"борис", Address{"Сочи", "354000"}} — порядок значений: сначала поля Person, внутри — поля Address. Третий: напечатайте p.Home целиком — {Казань 101000}.

```go
package main

import "fmt"

// Address — адрес: город и индекс
type Address struct {
        City string
        Zip  string
}

// Person — человек: имя и домашний адрес
type Person struct {
        Name string
        Home Address
}

func main() {
        p := Person{Name: "аня", Home: Address{City: "Москва", Zip: "101000"}}
        fmt.Println(p.Name, p.Home.City)
        p.Home.City = "Казань"
        fmt.Println(p)
}
```

### Встраивание: поля без имени, но не наследование

Встраивание (embedding) выглядит как поле, у которого указан только тип, без имени: внутри Person стоит строка Address — и это не пропущенное имя, а особый вид поля. Доступ к полям встроенного типа работает напрямую: не p.Address.City, а просто p.City — язык «поднимает» поля внутреннего типа на внешний уровень, и они ведут себя как свои. Встраивают не только структуры: любой тип с полями или методами можно встроить. Методы встроенного типа тоже поднимаются — это главная практическая причина встраивания, и до неё мы дойдём в следующем уроке о методах.

Теперь честная оговорка, которую вы слышали в каждом уроке: песочница живёт на двух движках, и на встраивании они расходятся. Основной движок и настоящий Go исполняют пример ниже одинаково — печатается «Москва аня». Резервный движок промоутед-поля не поддерживает: обращение p.City у него завершается ошибкой «у типа Person нет поля или метода City». Поэтому блок ниже неранимый — кнопки запуска у него нет, а вывод обоих взрослых движков записан в объяснении.

И главное — про миф. Встраивание НЕ наследование, и разница принципиальная. При наследовании дочерний тип ЕСТЬ разновидностью родительского: подмена, полиморфизм, переопределение поведения. В Go нет ни того ни другого: Person не «является» Address — Person СОДЕРЖИТ Address, просто к содержимому ведёт короткая дорога. Внутри p по-прежнему лежит отдельное поле с типом Address, к нему можно обратиться явно — p.Address.City, — а два имени (City снаружи и Address.City) указывают на одно и то же место. Конфликт имён разрешается просто: если у внешнего типа есть собственное поле City, оно выигрывает, и до встроенного ведёт длинная запись. Это композиция с удобным доступом, а не родство типов; держите эту формулировку — она сэкономит вам часы споров в комментариях под статьями.

### Встраивание в действии

```go
package main

import "fmt"

type Address struct {
        City string
}

type Person struct {
        Address // встроенное поле: тип вместо имени
        Name    string
}

func main() {
        p := Person{Address: Address{City: "Москва"}, Name: "аня"}
        fmt.Println(p.City, p.Name)
        fmt.Println(p.Address.City)
}
```

Кнопки нет: резервный движок не поддерживает промоутед-поля и падает с «у типа Person нет поля или метода City» ещё на первом обращении. Основной движок песочницы и настоящий Go исполняют честно, оба печатают две строки: «Москва аня» и «Москва». Первая печать берёт город напрямую — поле City поднялось с уровня Address; вторая идёт по полному пути p.Address.City и показывает, что встроенное поле осталось обычным полем со своим именем по типу. В ранимых примерах урока встраивания нет — теперь вы знаете, почему, и как читать такие места в чужом коде.

### Сравнение структур: == работает, но не для всех полей

Две структуры сравниваются оператором == — если сравнимы все их поля. Числа, строки, булевы, указатели — сравнимы; срезы и карты — нет. Отсюда правило: структура со срезом или картой внутри оператором == не сравнивается вовсе — настоящий Go не компилирует такой код с сообщением про несравнимый тип, а не «возвращает false», как можно было бы ожидать. Компилируемая проверка равенства для таких структур собирается вручную: сравнивайте поля по одному или пишите функцию Equal(a, b Bag) bool, которая решает, что считать равенством для срезов.

Для сравнимых структур == сопоставляет поля попарно: a == b истинно, когда каждое поле a равно соответствующему полю b. Это делает == удобным в тестах и проверках: соберите ожидаемую структуру литералом и сравните с полученной — вместо кучи отдельных if. Полезно знать, что сравнение структур с одинаковыми значениями работает и когда они собраны разными способами: именованный литерал с дописанным нулём равен позиционному — вы видели это в predict выше: вид записи не влияет на значение.

Указатели на структуры сравниваются иначе: p == q истинно только когда оба указателя смотрят на ОДНУ И ТУ ЖЕ переменную в памяти. Содержимое сравнивают разыменованием: *p == *q — сравнение структур по значениям полей. Различие «адрес или содержимое» — та же развилка, что была в 22-м при разговоре о nil: сначала решите, что именно вы хотите считать равенством, потом выбирайте запись. Копия структуры и оригинал равны по содержимому (*p == *q истинно), но разные по адресу (p == q ложно) — обе половины проверены пробником на обоих движках.

Отдельно про порядок и историю: раньше == на структурах использовали осторожно, если в полях прятались сравнимые, но с неожиданной семантикой вещи вроде указателей на одно и то же — сегодня правило одно: сравнимы все поля — сравниваем смело; внутри срез или карта — == закрыт, пишем свою функцию. В следующем quiz проверите себя ровно на этом водоразделе.

### Что выведет код: карта структур

```go
package main

import "fmt"

type Point struct {
        X int
        Y int
}

func main() {
        pts := map[string]Point{
                "a": {1, 2},
                "b": {3, 4},
        }
        p := pts["a"]
        p.X = 99
        fmt.Println(pts["a"].X, p.X)
}
```

**Что выведет программа?**

- ✅ 1 99: p — копия значения из карты, правка копии карту не трогает
- 99 99: значения карты — ссылки, правка через прочитанное значение видна в карте
- 0 99: чтение из карты затёрло элемент нулевой структурой
- Ошибка исполнения: нельзя присваивать полю значения, прочитанного из карты

Значения этой карты — структуры Point, и чтение pts["a"] возвращает КОПИЮ структуры: семантика значения работает и внутри карт. p.X = 99 меняет копию — карта хранит прежние {1 2}, печать даёт «1 99». Это тот же контраст, что в 19-м: сама карта — ссылочный тип, но её ЗНАЧЕНИЯ копируются при чтении. Чтобы править по месту, перепишите элемент целиком: pts["a"] = p — или храните в карте указатели map[string]*Point и правьте по адресу. Заметим на полях: запись pts["a"].X = 99 прямо в поле элемента карты настоящим Go запрещена на этапе компиляции — значения карты нельзя править по частям через индекс; песочница этот запрет не воспроизводит, поэтому в ранимых примерах правим только копию или элемент целиком.

### Проверьте себя: границы сравнения

**Вопрос:** type Bag struct { Items []int }; a := Bag{Items: []int{1}}; b := Bag{Items: []int{1}}. Что произойдёт при a == b?

- ✅ Программа не скомпилируется: поле-срез несравнимо, оператор == для такой структуры запрещён
- Напечатается true: срезы сравниваются поэлементно
- Напечатается false: срезы всегда считаются разными
- Напечатается false, а при равных указателях — true: срезы сравниваются как ссылки

Сравнимость структуры определяется её худшим полем: если хоть одно поле — срез, карта или функция, == для всей структуры не работает, и настоящий Go отклоняет код при компиляции. Поэлементное сравнение срезов языком не предусмотрено — срез можно сравнить только с nil. Для Bag пишут свою проверку равенства: вручную по полям или функцией Equal со срез-логикой. Песочница типы при == не проверяет и молча выдаёт false — ещё одна причина знать правило, а не полагаться на инструмент.

### Карта структур: коллекция составных значений

Самая частая коллекция структур — карта: ключ опознаёт элемент, а структура хранит его данные. Объявление читается слева направо: map[string]Point — «карта, у которой ключи — строки, а значения — структуры Point». Литерал собирается в знакомом виде из [урока о картах](/courses/go-basics/lessons/19-karty/), и здесь работает приятное сокращение: внутри литерала карты имя типа элементов можно опустить — "a": {1, 2} вместо "a": Point{1, 2}; язык знает тип значений из объявления карты и додумывает литерал сам. Оба движка исполняют элизию типа одинаково — она есть в ранимом predict выше.

Чтение — по знакомой точке: pts["a"].X; comma-ok тоже на месте: v, ok := pts["b"] — вторая переменная честно скажет, был ли ключ. А вот правка требует одной осторожной мысли: прочитанное значение — это КОПИЯ структуры, и правка копии карту не меняет. Рабочие приёмы два: перезаписать элемент целиком — pts["a"] = p, — или хранить в карте указатели: map[string]*Point, тогда p := pts["a"]; p.X = 99 меняет оригинал, потому что p — адрес. Второй приём вы уже умеете читать после 22-го: звёздочка в типе значений карты — сигнал «элементы правятся на месте».

Отдельный запрет, о котором стоит знать заранее: pts["a"].X = 99 — присваивание полю элемента карты — настоящим Go не компилируется. Причина в механике карты: элементы переезжают при росте, и язык не позволяет держать изменяемый адрес поля внутри элемента. Срез структур устроен мягче: pts[1].X = 99 в срезе легален — элементы среза живут по стабильным адресам, вы это уже видели при правке среза через адреса в 22-м. Итог: срез структур — для упорядоченных списков, карта структур — для lookup по ключу.

Цикл по карте структур пишется обычным range: for name, p := range pts — вторая переменная цикла получает копию структуры, читайте поля смело, правьте через перезапись или указатели. Порядок обхода, как всегда у карт, не гарантирован — если нужен порядок, собирайте ключи в срез и сортируйте тем же приёмом, что в уроке о картах.

### Структура или карта: как выбрать

Структура и карта решают разные задачи, и выбор обычно очевиден, как только сформулировать вопрос. Структура — для данных с ЗАРАНЕЕ ИЗВЕСТНЫМ набором полей: у каждого заказа есть номер и сумма, у каждой карточки — номинал и масть. Состав полей фиксируется в типе, компилятор проверяет каждое обращение: опечатка pl.Scroe не соберётся — «нет поля Scroe». Карта — для данных с ДИНАМИЧЕСКИМИ ключами: счётчик слов, настройки по именам, кэш по URL. Нельзя заранее перечислить ключи — значит, это работа карты.

Отсюда и практические различия. Поля структуры — это контракты: их конечность видна в объявлении, тип каждого поля проверен, отсутствующее поле не «нулевое значение по соглашению», а честный ноль своего типа. Ключи карты — это данные: их множество меняется в исполнении, обращение к отсутствующему ключу возвращает нулевое значение типа (и comma-ok отличает «нет ключа» от «лежит ноль»). Доступ к полю структуры — мгновенный и проверяемый на этапе компиляции; доступ по ключу карты — с хэшированием и без проверки компилятором смысла ключа.

Антипаттерн, на котором спотыкаются: моделировать структуру картой map[string]string — "name": "аня", "score": "10". Пока полей два, живётся нормально; на пятом поле появляется словарь магических строк вместо подсказок компилятора, а значения приходится конвертировать из строк туда и обратно. Первые же опечатки в ключах проходят молча. Правило: известные поля — структура; произвольные ключи — карта; а если данных очень много и нужны оба мира, у языка есть составные варианты — структура со срезом или картой внутри, что вы только что видели на Person с Address.

И про следующий шаг: структура, которая умеет не только хранить поля, но и отвечать на вызовы levelUp() — это уже методы. Они объявляются с приёмником — особым параметром, который пишет тип функции перед её именем, — и превращают структуры в полноценные типы с поведением. Это сюжет следующего урока; здесь остаёмся в мире данных.

### Печать структур: %v и %+v

Структура печатается fmt.Println-ом в компактной форме: {аня 10} — значения полей по порядку объявления, через пробел, в фигурных скобках. Вложенность раскрывается вложенными скобками: {аня {Казань 101000}}. Этого хватает для быстрых опытов, но при шести полях строка {аня 10 true 0 5 Москва 3} превращается в загадку: какое число что значит?

Для отладки у fmt есть глагол %+v: он печатает структуру вместе с именами полей — {Name:аня Score:10}. Разница между %v и %+v ровно одна — плюс-флаг просит formatter показать имена. Для вложенных структур имена появляются на каждом уровне, и структура данных читается глазами без заглядывания в объявление типа. Глагол %#v существует отдельно: он печатает значение в виде готового литерала Go — но это уже экзотика для глубокой отладки, достаточно знать о его существовании.

Как вы уже догадались по ритму урока, %+v — ещё одно место расхождения движков. Настоящий Go и основной движок печатают {Name:аня Score:10}; резервный движок флаг + не поддерживает и печатает сам шаблон «%+v» вместо значения. Поэтому блок ниже неранимый, а в ранимых примерах урока структуры печатает fmt.Println — его вывод одинаков в обоих движках. В реальной работе, где код исполняет настоящий Go, %+v — ваш основной инструмент заглядывания в структуры: printf-отладка структур без него втрое дольше.

Пара привычек на будущее. Во-первых, печать больших структур в цикле замедляет программу заметнее, чем кажется, — в горячем коде её убирают. Во-вторых, у структуры может быть собственный метод String() string, и тогда fmt печатает его результат вместо скобок — но методы за горизонтом этого урока, запомните фразу и встретимся с ней в следующий раз.

### %+v: печать с именами полей

```go
package main

import "fmt"

type Player struct {
        Name  string
        Score int
}

func main() {
        pl := Player{Name: "аня", Score: 10}
        fmt.Printf("%+v\n", pl)
        fmt.Printf("%v\n", pl)
}
```

Кнопки нет: резервный движок не понимает флаг + и печатает сам шаблон «%+v» вместо значения. Основной движок песочницы и настоящий Go исполняют честно и дают две строки: «{Name:аня Score:10}» и «{аня 10}» — с именами полей и без. Первый вывод собирается глаголом %+v, второй — %v; разница только в плюс-флаге. %v в резерве работает, поэтому в ранимых примерах урока структуры печатаются fmt.Println-ом — его вывод одинаков на обоих движках. Теперь при чтении чужого кода вы знаете, что означает запись с Name: внутри фигурных скобок — это печать структур с %+v.

### Шпаргалка: структуры

| Запись | Что делает | В песочнице |
| --- | --- | --- |
| type Card struct { ... } | объявление нового типа-структуры с полями | работает |
| Name, City string | групповая форма: несколько полей одного типа | работает |
| Card{Rank: "7", Suit: "черви"} | именованный композитный литерал — идиоматичен | работает |
| Card{"8", "пики"} | позиционный литерал: все поля по порядку | работает; пропуски настоящий Go не компилирует |
| Card{Rank: "дама"} | частичный именованный: остальное — нули | работает |
| pl.Name, pl.Score += 5 | доступ и правка полей через точку | работает |
| b := a; b.X = 99 | копия структуры: правка копии не видна | работает |
| p := &pl; p.Name = "..." | поля через указатель — сахар (*p).Name | работает; мост к уроку 22 |
| (*p).Name | полная форма: разыменование и поле | работает |
| var p Player | нулевая структура: { 0} | работает |
| a == b | сравнение структур с сравнимыми полями | работает; со срезом/картой внутри настоящий Go не компилирует |
| p == q / *p == *q | указатели: адрес против содержимого | работает |
| Person{Home: Address{...}} | вложенный литерал, доступ p.Home.City | работает |
| type Person struct { Address; Name } | встраивание: промоутед-поле p.City | основной yaegi да; резерв не поддерживает — неранимо |
| fmt.Println(pl) | компактная печать {аня 10} | работает |
| fmt.Printf("%+v", pl) | печать с именами полей {Name:аня Score:10} | резерв печатает сам шаблон «%+v» — неранимо |
| map[string]Point{"a": {1, 2}} | карта структур, элизия типа в литерале | работает; правим копию или перезаписываем элемент |
| func levelUp(p *Player) | указатель на структуру как параметр | работает; per-param f(p *Player, d int) резерв не парсит |

### Вставьте пропуски: карточка игрока

Соберите фрагмент: объявление типа используется трижды — в литерале, в правке и в чтении. Собранный код печатает «аня», затем 42.

```go
type Card struct {
        Name  string
        Score int
}

c := ___{Name: "аня"}
fmt.Println(c.Name)
___ = 42
fmt.Println(c.___)
```

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

Правильные ответы в пропусках: `Card`, `c.Score`, `Score`

### Соберите программу: счёт игрока

Соберите программу целиком: тип, главный блок, именованный литерал, правка поля и печать структуры. Собранный вывод — «{борис 8}».

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `type Player struct {`
4. `        Name  string`
5. `        Score int`
6. `}`
7. `func main() {`
8. `        pl := Player{Name: "борис", Score: 3}`
9. `        pl.Score += 5`
10. `        fmt.Println(pl)`
11. `}`

> **Честно о песочнице: два движка.** Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: объявление типа и групповая форма полей, именованные, позиционные и частичные литералы, доступ и правка полей, копирование структур b := a, поля через указатель (сахар p.Name и полная форма (*p).Name), нулевая структура var p Player со печатью { 0}, вложенные структуры и вложенные литералы обоих видов, сравнение == на сравнимых полях, сравнение указателей p == q и содержимого *p == *q, карты структур с элизией типа и comma-ok, срезы структур, пустая структура struct{}, печать fmt.Println(pl) в фигурных скобках, nil-поля-указатели. Расходятся или недоступны, все случаи проверены пробником: (1) %+v — резерв печатает сам шаблон «%+v», настоящий Go и основной yaegi — {Name:аня Score:10}; блок неранимый; (2) встраивание — резерв не поддерживает промоутед-поля («у типа Person нет поля или метода City»), yaegi и настоящий Go работают; блок неранимый; (3) смешанный литерал и позиционный с пропусками — настоящий Go не компилирует (mixture of field:value / too few values), резерв молча принимает и заполняет нулями — в ранимых только полные литералы; (4) присваивание полю элемента карты pts["a"].X = 99 — настоящий Go не компилирует, резерв молчит — в ранимых правим копию или элемент целиком; (5) == у структуры со срезом/картой — настоящий Go отклоняет при компиляции, резерв выдаёт false без проверки типов — в ранимых == только на сравнимых полях; (6) per-param сигнатуры func levelUp(p *Player, d int) — валидны в настоящем Go, резерв не парсит («ожидался тип») — в ранимых один параметр или групповая форма.

### Карточки: структуры

- **Как объявить структуру и создать значение?** — type Card struct { Rank string; Suit string } — новый тип с полями. Значение — композитный литерал: именованный Card{Rank: "7", Suit: "черви"} (идиоматичен: имена видны, порядок свободен, пропуски дают нули) или позиционный Card{"8", "пики"} (все поля строго по порядку). Смешивать формы в одном литерале нельзя.
- **Что такое нулевая структура?** — var p Player даёт структуру с нулевыми значениями всех полей: числа 0, строки "", булевы false, указатели nil. Печатается как { 0}. Именованный литерал с пропусками Player{Name: "аня"} — та же механика: пропущенное поле получает ноль своего типа. Нулевая структура — не nil; nil бывает только у указателя на структуру.
- **Почему p.Name работает, хотя p — указатель?** — Это синтаксический сахар: p.Name разворачивается в (*p).Name — сначала разыменование, потом поле. Одна форма для значения и указателя, без стрелок из C. Правка через указатель меняет оригинал: p.Score += 5 внутри функции levelUp(p *Player) видна снаружи. Копия структуры b := a независима от a.
- **Что такое встраивание (embedding) и чем оно НЕ является?** — Поле без имени: type Person struct { Address; Name string }. Поля Address поднимаются: p.City вместо p.Address.City. Это НЕ наследование: Person содержит Address, а не «является» им; полиморфизма и переопределения нет, внутреннее поле доступно явно — p.Address.City. Методы встроенного типа тоже поднимаются — мост к уроку о методах.
- **Когда структуры сравниваются оператором ==?** — Если ВСЕ поля сравнимы (числа, строки, булевы, указатели) — == сопоставляет поля попарно. Поле-срез или поле-карта делают == невозможным: настоящий Go не компилирует. Указатели сравнивают по адресу (p == q), содержимое — разыменованием (*p == *q). Структуры, собранные разными видами литералов, с равными полями равны.
- **Чем структура лучше карты для данных?** — Поля известны заранее: компилятор проверяет имена и типы (pl.Scroe не соберётся), нулевые значения честные, поля можно группировать и вкладывать. Карта — для динамических ключей, неизвестных на этапе написания. Антипаттерн — map[string]string вместо структуры: магические строки-ключи и конвертации. Карта структур map[string]Point соединяет lookup по ключу с типизированными значениями.

## Коротко: что запомнить

- Структура — свой тип из полей: type Card struct { Rank string; Suit string }; поля группируются построчно или групповой формой Name, City string.
- Композитный литерал двух видов: именованный Card{Rank: "7"} — идиоматичен, допускает пропуски (нули) и любой порядок; позиционный Card{"8", "пики"} требует всех полей по порядку; смешивать нельзя.
- Доступ через точку: pl.Name читает, pl.Score += 5 правит; через указатель — тот же синтаксис: p.Name — сахар для (*p).Name, мост к уроку 22.
- Присваивание копирует структуру целиком: b := a — независимая копия; править оригинал из функции — через указатель.
- Нулевая структура var p Player — это нули всех полей ({ 0}), не nil; нулевые значения должны быть осмысленным состоянием.
- Вложенные структуры собираются вложенным литералом и читаются цепочкой точек p.Home.City; встраивание поднимает поля безымянного типа — это композиция, не наследование.
- == сравнивает структуры с сравнимыми полями попарно; срез или карта в поле закрывают ==; указатели сравниваются по адресу, содержимое — через *p == *q.
- Карта структур map[string]Point хранит копии: правьте через перезапись элемента или указатели; pts["a"].X = 99 настоящий Go не компилирует.
- %v печатает значения полей, %+v добавляет имена ({Name:аня Score:10}) — резервный движок %+v не поддерживает, в песочнице печатайте fmt.Println.

## Домашнее задание

Соберите библиотеку карточек на структурах. Шаг первый: type Card struct { Word string; Transl string; Level int } и три карточки именованными литералами, одна — с пропуском уровня (он станет нулём). Шаг второй: напечатайте каждую карточку fmt.Println-ом и сравните две из них == — убедитесь, что разные слова дают false. Шаг третий: соберите карту map[string]Card с ключом-словом и найдите карточку по ключу с comma-ok; правьте уровень только у КОПИИ и напечатайте обе — карта должна остаться прежней. Шаг четвёртый: перезапишите элемент целиком pts["слон"] = updated и убедитесь, что теперь карта поменялась. Шаг пятый: type Deck struct { Name string; Cards []Card } — колода со срезом карточек внутри; наполните её двумя карточками и напечатайте len(deck.Cards). Попробуйте в комментариях записать, что вернёт deck == Deck{} и почему компилятор ответит отказом. [Оглавление курса](/courses/go-basics/) под рукой; в следующем уроке карточки обзаведутся методами.

## Вопросы и ответы по уроку

### Чем структура отличается от карты и когда какую выбирать?

Структура — для данных с заранее известным набором полей: имена полей зафиксированы в типе, компилятор проверяет каждое обращение, тип каждого поля свой. Карта — для пар «ключ — значение», где множество ключей неизвестно до исполнения: счётчики, кэши, настройки. Моделировать структуру картой map[string]string — антипаттерн: магические строки вместо проверки компилятором. Соединяются оба мира картой структур map[string]Card: ключ — идентификатор, значение — типизированные данные.

### Почему a == b не компилируется, если у структуры есть поле-срез?

Сравнимость структуры определяется её полями: числа, строки, булевы и указатели сравнимы, а срезы, карты и функции — нет, язык не определяет для них равенство. Структура с хотя бы одним несравнимым полем теряет оператор == целиком — настоящий Go отклоняет код при компиляции, а не возвращает false. Выход — своя функция Equal, сравнивающая поля вручную, или срез-логика внутри неё. Резервный движок песочницы проверку сравнимости не делает и молча выдаёт false — на это полагаться нельзя.

### Встраивание — это же наследование, верно?

Нет, и разница принципиальна. Встраивание embedding — это поле без имени: type Person struct { Address }; поля (и позже методы) встроенного типа «поднимаются» и доступны напрямую — p.City. Но Person не является разновидностью Address: подмены типов, полиморфизма и переопределения поведения нет. Внутри лежит обычное поле, доступное по имени типа — p.Address.City, — а собственное поле внешнего типа с тем же именем выигрывает у встроенного. Это композиция с коротким доступом; наследования в Go нет вовсе.
