Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 23 из 25 ~22 мин +50 XP

Структуры

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

Содержание урока · 15 разделов

Структуры в 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, оно выигрывает, и до встроенного ведёт длинная запись. Это композиция с удобным доступом, а не родство типов; держите эту формулировку — она сэкономит вам часы споров в комментариях под статьями.

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

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 проверите себя ровно на этом водоразделе.

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

Прочитайте код и выберите, что он выведет:

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: печать с именами полей

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.

type Card struct { Name string Score int } c := {Name: "аня"} fmt.Println(c.Name) = 42 fmt.Println(c.)

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

Термин

Как объявить структуру и создать значение?

нажмите, чтобы перевернуть
Определение

type Card struct { Rank string; Suit string } — новый тип с полями. Значение — композитный литерал: именованный Card{Rank: "7", Suit: "черви"} (идиоматичен: имена видны, порядок свободен, пропуски дают нули) или позиционный Card{"8", "пики"} (все поля строго по порядку). Смешивать формы в одном литерале нельзя.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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/) под рукой; в следующем уроке карточки обзаведутся методами.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Структуры)

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

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

Структура — для данных с заранее известным набором полей: имена полей зафиксированы в типе, компилятор проверяет каждое обращение, тип каждого поля свой. Карта — для пар «ключ — значение», где множество ключей неизвестно до исполнения: счётчики, кэши, настройки. Моделировать структуру картой 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 Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления