Содержание урока · 15 разделов
- Структуры в Go: свой тип из полей
- Объявление типа: поля, порядок, групповая форма
- Композитный литерал: именованный и позиционный
- Доступ к полям: точка и мост из урока об указателях
- Нулевая структура: объявили — уже работает
- Вложенные структуры: поле как целая структура
- Встраивание: поля без имени, но не наследование
- Сравнение структур: == работает, но не для всех полей
- Карта структур: коллекция составных значений
- Структура или карта: как выбрать
- Печать структур: %v и %+v
- Шпаргалка: структуры
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Структуры в Go: свой тип из полей#
До этого момента вы пользовались чужими типами: int, string, bool, срезами и картами. Это готовая мебель, но у неё предел: карта из урока о картах хранит пары «ключ — значение», однако не говорит, ЧТО именно лежит внутри и какие ключи обязательны. Игрок — это имя, счёт и уровень; заказ — номер, сумма и дата. Держать такие наборы в трёх отдельных переменных значит каждый раз передавать их по три штуки и надеяться, что не перепутаешь порядок. Структуры 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 — поля правятся так же свободно, как обычные переменные.
Запустите код, чтобы увидеть вывод.
Объявление типа: поля, порядок, групповая форма#
Разберём анатомию объявления. 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 }. Команда пишет новый литерал. Какой вариант и почему считается идиоматичным?
Доступ к полям: точка и мост из урока об указателях#
Обращение к полю — точка: pl.Name читает, pl.Score = 42 пишет, pl.Score += 5 читает-считает-пишет одним движком выражения. Точка работает на любых полях любых типов: у строкового поля — строковые операции, у числового — арифметика, а у поля-структуры — дальше по цепочке, о чём чуть ниже.
В уроке 22 вы уже трогали структуры через указатели: функция 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 есть нулевое значение — то, что лежит в переменной до первого присваивания. Для структуры это нулевые значения всех полей сразу: числа — 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: там каждая строка if err != nil спрашивала «пуста ли ошибка» — тот же механизм «пустой ссылки», применённый к другому типу. Нулевая структура безопасна всегда; нулевой указатель — под контролем проверки.
Что выведет код: нули и равенство
Прочитайте код и выберите, что он выведет:
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)
}Вложенные структуры: поле как целая структура#
Полю структуры можно дать любой тип — в том числе другую структуру. Так данные собираются в дерево: у человека есть домашний адрес, у адреса — город и индекс. Объявление ничем не отличается от обычного поля: 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}.
Запустите код, чтобы увидеть вывод.
Встраивание: поля без имени, но не наследование#
Встраивание (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, оно выигрывает, и до встроенного ведёт длинная запись. Это композиция с удобным доступом, а не родство типов; держите эту формулировку — она сэкономит вам часы споров в комментариях под статьями.
Встраивание в действии
GOpackage 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 проверите себя ровно на этом водоразделе.
Что выведет код: карта структур
Прочитайте код и выберите, что он выведет:
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)
}Проверьте себя: границы сравнения
type Bag struct { Items []int }; a := Bag{Items: []int{1}}; b := Bag{Items: []int{1}}. Что произойдёт при a == b?
Карта структур: коллекция составных значений#
Самая частая коллекция структур — карта: ключ опознаёт элемент, а структура хранит его данные. Объявление читается слева направо: map[string]Point — «карта, у которой ключи — строки, а значения — структуры Point». Литерал собирается в знакомом виде из урока о картах, и здесь работает приятное сокращение: внутри литерала карты имя типа элементов можно опустить — "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: печать с именами полей
GOpackage 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.
Соберите программу: счёт игрока
Соберите программу целиком: тип, главный блок, именованный литерал, правка поля и печать структуры. Собранный вывод — «{борис 8}».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Каждый ранимый пример урока прогнан пробником на обоих движках песочницы. Совпадают: объявление типа и групповая форма полей, именованные, позиционные и частичные литералы, доступ и правка полей, копирование структур 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, резерв не парсит («ожидался тип») — в ранимых один параметр или групповая форма.
Карточки: структуры
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Структура — свой тип из полей: 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 нет вовсе.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Продвинутые возможности Go: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления