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

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

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

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

Указатели

Указатели в go — тема, вокруг которой новички напридумывали больше всего страшилок, и ровно поэтому её стоит разобрать спокойно и на живых примерах. Урок 20 закончился ограничением: функция не может изменить переменную снаружи, потому что получает копию. Указатель снимает это ограничение: p := &x берёт адрес переменной, *p работает со значением по адресу — и оригинал меняется. Здесь разберём амперсанд и звёздочку, типы *T, nil-указатель, правку структуры через указатель, new() и случаи, когда адреса не нужны. Каждый ранимый пример прогнан на обоих движках песочницы.

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

Указатели в Go: адрес вместо копии#

В уроке о функциях мы приняли правило языка: аргументы передаются по значению — функция получает копию, и какие-нибудь правки внутри остаются в копии. Мы даже написали функцию tryChange(n int), которая честно присваивала своей копии 99, — а снаружи переменная не менялась. Сегодня этот предел обходится: указатели в go дают функции не копию значения, а адрес оригинала из памяти. Функция, получившая адрес, работает уже не с копией значения, а с самим оригиналом. Механика из двух символов: амперсанд & берёт адрес, звёздочка * разыменовывает указатель — читает или пишет значение по адресу.

Указатель — обычная переменная, только хранит он не число и не строку, а адрес другой переменной. Аналогия с домами работает хорошо: значение — это то, что лежит в квартире; адрес — это улица и номер; указатель — записка, на которой адрес написан. Копия записки стоит копейки, но ведёт в ту же квартиру: что бы вы ни принесли по этому адресу, окажется в исходной квартире, а не в копии. Именно поэтому правка через указатель видна снаружи, а правка копии значения — нет.

Справедливо и обратное чтение: если функция меняет переменную снаружи, значит, ей передали адрес. Взгляд на сигнатуру func bump(p *int) сразу говорит: эта функция может изменить то, на что ей указали. Звёздочка в типе — документация. Карту из урока о картах мы правили из функции без всяких амперсандов — и это не противоречие: у значений карт ссылка уже лежит внутри, язык сделал это за вас; указатель — тот же приём, только выполненный вручную и на виду.

В этом уроке: две роли звёздочки и амперсанда, типы вида *T, nil-указатель и проверка перед разыменованием, указатель-параметр как ответ на задачу из урока 20, правка структуры через p.Field, функция new() и честный разговор о том, когда указатели не нужны, а когда без них никуда.

Песочница: первый адрес

Запустите: x := 5 — обычная переменная; p := &x — в p лег адрес x, и тип p выводится как *int, «указатель на int». Первая печать *p читает значение по адресу — 5. Строка *p = 42 пишет по адресу — и обычная печать x показывает 42: оригинал изменился, хотя мы трогали только p. Третья строка *p = *p + 1 читает по адресу, прибавляет единицу и пишет обратно — x стало 43. Опыты. Первый: добавьте q := &x и напечатайте *q — обе записки ведут в одну квартиру. Второй: поменяйте x = 100 после взятия адреса и напечатайте *p — указатель видит свежее значение, он не «фотография», а живая ссылка. Третий: попробуйте fmt.Println(p) — увидите адрес вида 0xc000...; в песочнице он фиктивный и от запуска к запуску не обязаны совпадать адреса даже в настоящем Go, поэтому честные примеры в этом уроке печатают значения, а не адреса.

Вывод

Запустите код, чтобы увидеть вывод.

Амперсанд и звёздочка: один символ, две работы#

Амперсанд & делает одну вещь: поставленный перед переменной в выражении, он возвращает её адрес. &x — адрес переменной x. Всё. Никакой магии: операция, у которой есть тип результата и значение, — как сложение, только результат не число, а адрес.

Звёздочка работает в двух ролях, и их важно не перепутать. Первая роль — в описании типа: int — это имя типа «указатель на int». В записи var p int звёздочка принадлежит типу: переменная p будет хранить адреса целых чисел. Вторая роль — в выражении: p перед указателем-значением означает разыменование — «пойди по адресу и работай со значением там». fmt.Println(p) читает по адресу, *p = 42 пишет по адресу. Одна и та же буква в типе и в выражении — это не одна операция, а две, и различает их контекст: рядом с типом — объявляем указатель, перед переменной-указателем — разыменовываем.

Умножение здесь ни при чём, хотя символ общий: Go различает три смысла звёздочки по месту — в типе, перед указателем, между двумя значениями (тогда умножение). Читается это мгновенно после недели практики, а пока при встрече чужого кода задавайте один вопрос: звёздочка стоит у ТИПА или у ЗНАЧЕНИЯ? У типа — речь о виде переменной, у значения — о действии перехода по адресу.

Пара & и взаимно обратна: &x достаёт адрес из переменной, p достаёт значение из адреса. &x — это снова x: взяли адрес и тут же по нему перешли. Компилятор даже сворачивает такую комбинацию. А вот обратный порядок — сначала, & потом — не существует: разыменовывать можно только уже имеющийся указатель, у «голого» числа адреса нет.

Тип *T: указатель на тип#

Раз у &x есть результат, у результата есть тип: для x типа int выражение &x имеет тип int — читается «указатель на int». Когда вы пишете p := &x, компилятор выводит этот тип сам; когда хочется объявить переменную заранее — var p int. Так же образуются string, float64, bool и указатели на любые другие типы, включая будущие структуры: Player. Звёздочка в типе — не операция, а часть имени: int такой же полноценный тип, как int, со своими операциями (& над переменной даёт его, над ним даёт int).

У каждого типа в Go есть нулевое значение, и у указателей оно особенное — nil: «ни на что не смотрит». Объявите var p *int без присваивания — в p лежит nil. Печать такого указателя даёт литерал <nil> — тот же, что вы видели у nil-ошибок в уроке 21: fmt.Println(p) напечатает <nil>, и оба движка песочницы делают это одинаково. nil — честное «записка пуста», а не мусорный адрес: пока указатель nil, язык не даст (при проверке) случайно уйти по несуществующему адресу.

Присваивание указателя — обычное: p = &x кладёт адрес; p = &y перекладывает записку на другой дом — теперь p это y. Переменная-указатель переживает сколько угодно переприсваиваний. Сравнение тоже работает: p == nil проверяет «пуста ли записка», p == q — «смотрят ли два указателя на один адрес». Из сравнений вырастает главный защитный приём урока: if p != nil { fmt.Println(p) } — разыменовываем только тогда, когда по адресу действительно что-то есть.

Существует и указатель на указатель — **T: записка, на которой написано, где лежит записка. Язык это позволяет, но в обычном коде встречается редко; для чтения чужих программ достаточно знать, что две звёздочки означают два перехода по адресу, — углубляться не будем.

Проверьте себя: роли звёздочки

В одной функции встречаются и *int в сигнатуре, и *p в теле. Что означает звёздочка в каждом месте?

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

Запустите: tryChange(v) передала копию — внутри функции n стало 99, но первая печать показывает 5: копия умерла вместе с функцией. realChange(&v) получила адрес: *p = 99 записала в оригинал — вторая печать 99. bump(&v) прибавила единицу по адресу — третья печать 100. Разница между двумя функциями ровно один символ в сигнатуре: n int против p *int, — а поведение противоположное. Опыты. Первый: уберите амперсанд в вызове realChange(v) — программа не скомпилируется: в *int нельзя положить int, типы не совпадают; верните & на место. Второй: вызовите bump(&v) трижды подряд — счётчик честно накопится: 101, 102, 103. Третий: напишите внутри realChange печать *p до присваивания — увидите 5: адрес приехал уже с актуальным значением оригинала.

Вывод

Запустите код, чтобы увидеть вывод.

Почему это работает: аргументы по значению ещё раз#

Ничто из виденного выше не отменяет правило урока 20 — аргументы всегда копируются. Функция realChange(p int) действительно получила копию. Только копируется не значение переменной, а адрес: восемь байт записки вместо целой квартиры. Копия адреса указывает в тот же дом, поэтому p = 99 пишет в оригинал. Противоречия нет: по значению передаётся всё, просто значением указателя является адрес. Эта формулировка — ключ к пониманию чужих споров о «передаче по ссылке» в Go: строго говоря, её нет; есть передача указателей по значению, и эффект тот же.

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

В англоязычных материалах — типичный запрос pointer golang — эта картина описывается парой терминов pass by value и pass a pointer; теперь вы знаете, что за ними стоит механизм, а не два разных языка. Практический вывод для чтения кода: видите & перед аргументом при вызове — ищите в вызываемой функции звёздочку и спрашивайте себя «что она тут меняет»; видите голое значение — функция ограничена своей копией.

nil-указатель: адрес, который ни на что не смотрит#

Разберёмся с nil как с полноправным значением, а не курьёзом. var p int без присваивания — это nil-указатель: переменная объявлена, типа у неё int, а адреса в ней нет. Записка без адреса. Печатается он как <nil>, сравнивается напрямую: p == nil истинно, p != nil ложно. Оба движка песочницы ведут себя одинаково — это можно и нужно проверять руками в ранимых примерах ниже.

Зачем нужен «пустой» указатель? Как знак «данных нет», и это легальное состояние, а не ошибка. Функция может принимать указатель и спокойно обрабатывать оба случая: если p == nil — вести себя по-другому или сообщить о пустоте; если p != nil — работать со значением. Передать nil в функцию можно буквально: show(nil) — и внутри она об этом узнает. Так выглядят необязательные параметры в идиоматичном Go: вместо придумывания «магического числа» типа -1 передают nil и проверяют.

Вы уже пользовались nil-сравнением, не думая об указателях: в уроке 21 каждая проверка if err != nil спрашивала «пуста ли ошибка». nil в языке один на все ссылочные типы — указатели, ошибки, позже каналы и карты; у указателя он при этом нулевое значение по умолчанию, а не присвоенный.

Обратная сторона: разыменовывать nil нельзя — идти по адресу, которого нет. В настоящем Go это паника: runtime error: invalid memory address or nil pointer dereference, программа падает. Песочница не даст упасть процессу, но тоже прервёт выполнение с ошибкой — сообщения движков разные, суть одна: до строк ниже дело не дойдёт. Защита одна и простая: любое разыменование — после проверки p != nil, если nil возможен. Следующий predict заставит вас пережить падение лично.

Что выведет код: разыменование nil

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

package main

import "fmt"

func main() {
        var p *int
        fmt.Println("старт")
        fmt.Println(*p)
        fmt.Println("финиш")
}

Песочница: nil под контролем

Запустите: первый вызов show(nil) — адреса нет, срабатывает ветка else, печатается «пусто»; разыменования в ней нет, поэтому падать нечему. Второй вызов show(&x) приезжает с настоящим адресом: проверка p != nil проходит, печатается «есть: 6». Одна проверка в начале функции делает её безопасной для обоих миров. Опыты. Первый: закомментируйте проверку if — второй вызов по-прежнему сработает, а вот show(nil) упадёт: разыменование nil, как в predict выше. Второй: добавьте в main ещё одну переменную y := 9 и вызовите show(&y) — функция честно напечатает «есть: 9»: один guard обслуживает сколько угодно переменных. Третий: сделайте var p *int на уровне main и передайте show(p) — снова «пусто»: необъявленный адрес это nil.

Вывод

Запустите код, чтобы увидеть вывод.

Указатель на структуру: p.Field — синтаксический сахар#

Самое частое применение указателей в реальном Go — структуры: большие составные значения, которые дорого копировать и которые часто нужно править из функций. Структуры заслуживают отдельного урока — в 23-м разберём их целиком; здесь возьмём минимум, чтобы потрогать указатели. type Player struct { Name string; Score int } объявляет тип с двумя полями; pl := Player{Name: «борис», Score: 3} создаёт значение. Всё, что вы знаете о правке переменных через адреса, переносится на структуры без изменений: levelUp(&pl) передаёт адрес структуры, функция с параметром p *Player правит оригинал.

Приятный сюрприз — синтаксис. Раз p хранит адрес, по логике читать поле надо было бы (p).Score: сначала разыменовать, потом взять поле. Так писать можно, но никто не пишет: Go позволяет p.Score — компилятор сам вставляет разыменование. Это синтаксический сахар: короткая форма, которая разворачивается в полную. В языке C исторически были два синтаксиса — (p).field и стрелка p->field; в Go одна форма на все случаи: p.Field и для значения, и для указателя. Никаких стрелок, никакого выбора — меньше поводов для ошибок.

Через указатель на структуру работает и запись: p.Name = «новое имя» меняет оригинал. Адрес можно взять и у поля: &pl.Score — это *int, указывающий на одно поле; bump(&pl.Score) прибавит единицу именно к счёту. Сравните с картами из урока 19: у элемента карты адрес взять нельзя, язык запрещает — а у поля структуры можно, пробник проверил это в обоих движках. Правило запомнить легко: поля структуры живут по фиксированным адресам, элементы карты переезжают при росте.

И последний штрих: адрес можно взять прямо у литерала — p := &Player{Name: «аня», Score: 10}. Так заводят указатель на свежую структуру без промежуточной переменной. Оба движка исполняют это одинаково — встретите такую запись в любом боевом коде.

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

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

Вывод

Запустите код, чтобы увидеть вывод.

Правка среза по адресам элементов

GO
package main

import "fmt"

// bump удваивает число по адресу
func bump(p *int) {
        *p = *p * 2
}

func main() {
        nums := []int{1, 2, 3}
        for i := range nums {
                bump(&nums[i])
        }
        fmt.Println(nums[0], nums[1], nums[2])
}

Кнопка есть: вывод одинаков в обоих движках — «2 4 6». Функция bump принимает int — и ей без разницы, чей это адрес: обычной переменной или элемента среза. &nums[i] берёт адрес i-го элемента, p = p 2 удваивает значение по адресу — срез меняется на месте. Range здесь идёт по индексам (урок про range): для правки по адресам нужен именно индекс, потому что адрес берётся у nums[i], а не у копии значения из второй переменной range. Тот же трюк работает с массивами и с полями структур; у элементов карт адреса нет. Это готовый шаблон «пройти по коллекции и отредактировать функцией, работающей с адресами».

Проверьте себя: адрес поля

pl := Player{Name: "борис", Score: 3}; затем вызвали bump(&pl.Score), где func bump(p *int) { *p = *p + 1 }. Что напечатает fmt.Println(pl.Score)?

new(T): указатель на нулевое значение

GO
package main

import "fmt"

func main() {
        p := new(int)
        fmt.Println(*p)
        *p = 12
        fmt.Println(*p)
}

Кнопки нет: резервный движок не знает new и падает с «new требует тип» ещё до исполнения. Основной движок песочницы (yaegi) и настоящий Go исполняют честно: new(int) выделяет место под нулевое значение int и возвращает указатель на него — первая печать 0; затем p = 12 пишет по адресу — вторая печать 12. То же работает для любых типов: new(Player) даст указатель на структуру с нулевыми полями. new(T) — короткая альтернатива паре «объявить переменную, взять адрес»: вместо x := 0; p := &x пишут p := new(int). В ранимых примерах урока new не встречается — резерв его не берёт, — но читать чужой код с ним теперь можно свободно: это тот же T, только адрес получен без промежуточной переменной.

Когда указатели не нужны: маленькие значения#

Указатель — инструмент, а не знак хорошего стиля сам по себе. Для маленьких значений — int, float64, bool, коротких строк — указатель усложняет жизнь без выгоды: копия и так стоит ничего, а код обрастает звёздочками, амперсандами и обязанностью думать о nil. Функция square(n int) читается мгновенно; функция square(p *int) заставляет читателя остановиться и спросить: зачем адрес? она собирается менять мой аргумент? Если ответ «нет» — адрес не нужен. Копия для маленьких значений дешевле любой лишней мысли о ней.

Рабочее правило из практики: функция должна изменить ваш аргумент — передавайте указатель, и амперсанд в вызове станет честным предупреждением «эта функция может править мою переменную». Не должна — передавайте значение. Значение большое — структура с десятками полей — попробуйте указатель ради экономии копирования; когда доберёмся до структур в 23-м уроке, у этого правила появится второе дыхание. Срезы и карты уже несут ссылку внутри: правка их элементов видна снаружи без всяких амперсандов, оборачивать их в указатель почти никогда не нужно.

Есть и обратная дисциплина: каждый указатель в сигнатуре — это возможный nil и обязанность решить, кто его проверяет. Функция, которая принимает p *int и разыменовывает без проверки, перекладывает ответственность на вызывающих — все они теперь должны гарантировать не-nil. Иногда это договор уместен и задокументирован; чаще — лишний риск. Отсюда финальный чек-лист перед взятием адреса.

Четыре вопроса перед взятием адреса#

  • Функция обязана изменить мою переменную? Да — передаю &x и жду *T в параметре. Нет — передаю значение как есть.
  • Значение большое — структура с кучей полей? Тогда указатель экономит копирование; для int и коротких строк выгода нулевая.
  • Это срез или карта? Ссылка уже внутри — правки элементов и так видны снаружи, указатель не нужен.
  • Может ли указатель оказаться nil, и кто это проверяет? Решите заранее: проверка в функции, проверка у вызывающего или гарантия «никогда не nil» — но осознанно.

Вставьте пропуски: счётчик через адрес

Соберите трёхстрочный фрагмент: берём адрес, читаем по нему, пишем по нему. Собранная программа печатает 5, затем 42.

x := 5 p := fmt.Println() = 42 fmt.Println(x)

Соберите программу: обмен через адреса

Соберите классический swap: функция с двумя указателями меняет местами значения двух переменных. Собранный вывод — «2 1».

Строки

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

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

Шпаргалка: указатели#

ЗаписьЧто делаетВ песочнице
x := 5; p := &xамперсанд берёт адрес переменной, p хранит егоработает
fmt.Println(*p)разыменование: чтение значения по адресуработает
*p = 42запись по адресу: меняется оригиналработает
var p *intnil-указатель: нулевое значение типа *intработает; печатается как <nil>
if p != nil { ... }проверка перед разыменованиемработает
func bump(p *int)указатель-параметр: функция правит переменную снаружиработает; один параметр или групповая форма
func swap(a, b *int)групповая сигнатура: два адреса в одной функцииработает; обмен значениями «2 1»
show(nil)передача nil в функцию с проверкой внутриработает
p := &nums[1]адрес элемента среза; правка по нему видна в срезеработает
bump(&pl.Score)адрес поля структурыработает; у элемента КАРТЫ адрес взять нельзя
p.Field = ...синтаксический сахар: разыменование без скобок и стрелокработает; структуры подробно в уроке 23
p := &Player{Name: "аня"}адрес литерала структуры без промежуточной переменнойработает
p := new(int)указатель на нулевое значение intосновной yaegi да; резерв не знает new — неранимо
fmt.Println(p)печать самого адресарезерв печатает фиктивный 0xc0000000 — неранимо
func f(p *Player, d int)параметры с отдельными типами (per-param)валидно в настоящем Go; резерв не парсит — в ранимых нет

Карточки: указатели

Термин

Что делают амперсанд & и звёздочка *?

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

&x берёт адрес переменной x — результат имеет тип *int. *p разыменовывает указатель: читает или пишет значение по адресу (*p = 42 меняет оригинал). Звёздочка у ТИПА (*int) — часть имени типа «указатель на int», у ЗНАЧЕНИЯ — операция разыменования, между двумя числами — умножение.

1 / 6

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • &x берёт адрес, *p разыменовывает; тип указателя — *T, нулевое значение — nil, печатается как <nil>.
  • Правка через указатель *p = 42 меняет оригинал: по значению передаётся адрес, а адрес ведёт в тот же дом.
  • func bump(p *int) правит переменную снаружи — снято ограничение урока 20; правка карты из урока 19 видна потому, что ссылка уже внутри значения.
  • nil-указатель — легальное «данных нет»: печатайте и сравнивайте, но разыменовывайте только после if p != nil; разыменование nil — паника в настоящем Go и ошибка в обоих движках песочницы.
  • Указатель на структуру: p.Field — синтаксический сахар без скобок и стрелок; levelUp(&pl) правит оригинал, &pl.Score — адрес одного поля.
  • new(T) возвращает указатель на нулевое значение; резервный движок его не знает — блок неранимый с честной нотой.
  • Маленькие значения передавайте как есть: указатель — инструмент правки и больших данных, у каждого *T в сигнатуре — договор о nil.

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

Соберите счётчик на адресах. Шаг первый: func bump(p *int), прибавляющая единицу по адресу, — вызовите её три раза на одной переменной и напечатайте результат: должно выйти 3. Шаг второй: func swap(a, b *int) — обменяйте местами значения двух переменных и напечатайте их после обмена. Шаг третий: тип type Counter struct { Name string; N int }, функция inc(c *Counter), поднимающая N на пять, — правьте и имя через локальный указатель. Шаг четвёртый: удвойте все элементы среза циклом по индексам с bump(&nums[i]) — используйте ранимый шаблон из урока. Шаг пятый: func show(p *int) с проверкой nil — вызовите её с nil, с адресом переменной и с адресом элемента среза; убедитесь, что ни один вызов не падает. [Оглавление курса](/courses/go-basics/) под рукой; структуры из урока 23 скоро позволят расширить счётчик настоящими методами.

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

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

Почему в песочнице адрес печатается как 0xc0000000 и всегда одинаковый?

Это фиктивное значение резервного движка: настоящий адрес он не моделирует, поэтому любое место в памяти выглядит одинаково. Основной движок и настоящий Go печатают реальные адреса вида 0xc0000120c8, и от запуска к запуску они меняются — адреса нельзя использовать как вывод программы или ожидаемый результат. Поэтому честные примеры этого урока печатают значения по адресам (*p), а не сами адреса: значения детерминированы, адреса — нет.

Чем & отличается от * — они не путаются?

Амперсанд применяется к переменной и достаёт адрес: &x — это *int. Звёздочка применяется к указателю и достаёт значение: *p — это int. Они взаимно обратны: *&x снова x. Единственная ловушка — та же звёздочка в типе: в var p *int и в сигнатуре func f(p *int) она не операция, а часть имени типа «указатель на int». Вопрос-подсказка при чтении: звёздочка стоит у типа или у значения?

Нужно ли всегда проверять p != nil перед разыменованием?

Там, где nil возможен, — да: разыменование nil падает (в настоящем Go — паника invalid memory address or nil pointer dereference), и компилятор эту точку не подсвечивает, беда случается при исполнении. Если указатель только что получен из & переменной и никуда не переприсваивался — он не nil, проверка избыточна. Функция, принимающая указатель, не знает, что ей приедет, — поэтому guard if p != nil в её начале либо явная договорённость в документации. Защитный паттерн из урока — show(nil) печатает «пусто» и не падает.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

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

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

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