---
title: "Указатели"
description: "Указатели в go: амперсанд берёт адрес, звёздочка разыменовывает. Типы *T, nil-указатель, правка переменной из функции, p.Field на структуре, new(T)."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/22-ukazateli/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 22
---

# Указатели

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

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

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

В [уроке о функциях](/courses/go-basics/lessons/20-funkcii/) мы приняли правило языка: аргументы передаются по значению — функция получает копию, и какие-нибудь правки внутри остаются в копии. Мы даже написали функцию 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, поэтому честные примеры в этом уроке печатают значения, а не адреса.

```go
package main

import "fmt"

func main() {
        x := 5
        p := &x
        fmt.Println(*p)
        *p = 42
        fmt.Println(x)
        *p = *p + 1
        fmt.Println(x)
}
```

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

Амперсанд & делает одну вещь: поставленный перед переменной в выражении, он возвращает её адрес. &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 в теле. Что означает звёздочка в каждом месте?

- ✅ *int — часть имени типа: «указатель на int»; *p — разыменование: работа со значением по адресу
- Оба раза это умножение: Go различает смысл по типу переменных
- *int — разыменование типа, а *p — создание нового указателя
- *p в теле функции запрещён: звёздочка живёт только в объявлениях типов

Две роли одной буквы: рядом с типом (*int в сигнатуре, var p *int) звёздочка образует имя типа «указатель на T»; перед переменной-указателем в выражении (*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: адрес приехал уже с актуальным значением оригинала.

```go
package main

import "fmt"

// tryChange получает копию значения — снаружи не видно
func tryChange(n int) {
        n = 99
}

// realChange получает адрес — правит оригинал
func realChange(p *int) {
        *p = 99
}

// bump прибавляет единицу по адресу
func bump(p *int) {
        *p = *p + 1
}

func main() {
        v := 5
        tryChange(v)
        fmt.Println(v)
        realChange(&v)
        fmt.Println(v)
        bump(&v)
        fmt.Println(v)
}
```

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

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

Сверьте ощущения с уже знакомыми типами. Число, строка, булево — функция получает копию и правит впустую. Карта из [урока о картах](/courses/go-basics/lessons/19-karty/) — правки внутри функции видны снаружи: значение карты — это заголовок, внутри которого живёт ссылка на данные; копируется заголовок, данные остаются общими. Со срезом то же самое — вы это видели, когда 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](/courses/go-basics/lessons/21-oshibki-i-vozvraty/) каждая проверка if err != nil спрашивала «пуста ли ошибка». nil в языке один на все ссылочные типы — указатели, ошибки, позже каналы и карты; у указателя он при этом нулевое значение по умолчанию, а не присвоенный.

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

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

```go
package main

import "fmt"

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

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

- ✅ «старт» напечатается, дальше паника при разыменовании nil — до «финиш» дело не дойдёт
- старт 0 финиш: разыменование nil даёт нулевое значение типа
- старт <nil> финиш: p напечатается, потом программа спокойно завершится
- Ничего: компилятор откажется собирать программу с разыменованием nil

Первая печать успевает: «старт» выходит до беды. Затем *p пытается перейти по адресу nil — и программа падает: в настоящем Go это паника invalid memory address or nil pointer dereference, в песочнице — ошибка выполнения (резервный движок пишет «разыменование не-указателя», основной — reflect-ошибку; сообщения разные, падение одно). Компилятор тут не помощник: типы корректны, беда случится только при исполнении. Лечение — проверка if p != nil перед разыменованием; runner ниже показывает её в деле. Печать самого nil-указателя — fmt.Println(p) — при этом безобидна: печатается <nil>, падает именно переход по адресу.

### Песочница: 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.

```go
package main

import "fmt"

// show печатает число по адресу, если он не пуст
func show(p *int) {
        if p != nil {
                fmt.Println("есть:", *p)
        } else {
                fmt.Println("пусто")
        }
}

func main() {
        show(nil)
        x := 6
        show(&x)
}
```

### Указатель на структуру: 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"

type Player struct {
        Name  string
        Score int
}

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

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

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

```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)?

- ✅ 4: адрес поля — тоже адрес; правка по нему меняет оригинал
- 3: поле скопировалось при передаче, функция меняла копию
- 0: после передачи по адресу поле обнуляется
- Ошибку компиляции: у поля структуры нельзя взять адрес

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

### 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.

```go
x := 5
p := ___
fmt.Println(___)
___ = 42
fmt.Println(x)
```

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

Правильные ответы в пропусках: `&x`, `*p`, `*p`

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

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

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

1. `package main`
2. `import "fmt"`
3. `func swap(a, b *int) {`
4. `        tmp := *a`
5. `        *a = *b`
6. `        *b = tmp`
7. `}`
8. `func main() {`
9. `        x := 1`
10. `        y := 2`
11. `        swap(&x, &y)`
12. `        fmt.Println(x, y)`
13. `}`

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

| Запись | Что делает | В песочнице |
| --- | --- | --- |
| x := 5; p := &x | амперсанд берёт адрес переменной, p хранит его | работает |
| fmt.Println(*p) | разыменование: чтение значения по адресу | работает |
| *p = 42 | запись по адресу: меняется оригинал | работает |
| var p *int | nil-указатель: нулевое значение типа *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 и разыменование *p, запись по адресу *p = 42, указатели-параметры в функциях — одиночные (p *int, p *Player) и групповые (a, b *int), правка переменной из функции, копия против адреса, p.Field и += по полю, адреса элементов среза и полей структур, nil-указатель, p == nil и p != nil, передача nil в функцию, печать nil как <nil>, переприсваивание указателя p = &y, чтение в выражениях (*p + 3), печать структуры {борис 8} и адреса литерала &Player{...}. Расходятся или недоступны, все случаи проверены пробником: (1) new(T) — резервный движок не знает new («new требует тип»), основной yaegi и настоящий Go работают — блок неранимый; (2) печать самого адреса — резерв показывает фиктивный 0xc0000000, настоящий Go выдаёт адрес, меняющийся от запуска к запуску, — в ранимых не печатаем; (3) разыменование nil — падают оба движка, но с разными сообщениями: резерв «разыменование не-указателя», основной — reflect-ошибка, настоящий Go — паника invalid memory address or nil pointer dereference; (4) per-param сигнатуры с указателем f(p *Player, d int) — настоящий Go валиден, резерв не парсит («ожидался тип») — в ранимых групповые формы и одиночные параметры; (5) имя функции max — резерв путает со встроенной функцией и печатает фиктивный адрес вместо результата, настоящий Go и основной движок корректны — таких имён в ранимых нет.

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

- **Что делают амперсанд & и звёздочка *?** — &x берёт адрес переменной x — результат имеет тип *int. *p разыменовывает указатель: читает или пишет значение по адресу (*p = 42 меняет оригинал). Звёздочка у ТИПА (*int) — часть имени типа «указатель на int», у ЗНАЧЕНИЯ — операция разыменования, между двумя числами — умножение.
- **Как объявить указатель и чему он равен без присваивания?** — var p *int — тип «указатель на int». Без присваивания p равен nil («ни на что не смотрит»), печатается как <nil>. Печать nil-указателя безобидна; разыменование nil — падение. Перед разыменованием проверяйте if p != nil — так функции спокойно принимают nil как «данных нет».
- **Почему функция с параметром p *int меняет переменную снаружи?** — Аргументы в Go всегда копируются (урок 20), но копия адреса указывает на ту же переменную: *p = 99 пишет в оригинал. С обычным int-параметром функция правила бы копию впустую. Передача «по ссылке» в Go отсутствует — есть передача указателя по значению, эффект тот же.
- **Что за синтаксический сахар p.Field у указателя на структуру?** — p.Name — то же, что (*p).Name: компилятор сам разыменовывает указатель перед обращением к полю. Никаких стрелок -> из C — один синтаксис для значения и указателя. Работает и запись (p.Name = ...), и адрес поля &pl.Score — это *int. Структуры целиком — урок 23.
- **Что делает new(T)?** — new(int) выделяет место под нулевое значение T и возвращает указатель: *p == 0; new(Player) — на структуру с нулевыми полями. Альтернатива паре «объявить переменную, взять адрес». Резервный движок песочницы new не знает («new требует тип») — неранимо; основной yaegi и настоящий Go исполняют.
- **Когда указатель НЕ нужен?** — Для маленьких значений (int, float64, bool, короткие строки): копия дёшева, а звёздочки и nil-проверки усложняют код. Срезы и карты уже «ссылочные» внутри. Указатель берут, когда функция обязана править оригинал (амперсанд в вызове — честный знак этого) или когда значение большое; каждый *T в сигнатуре — договор о возможном nil.

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

- &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 скоро позволят расширить счётчик настоящими методами.

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

### Почему в песочнице адрес печатается как 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) печатает «пусто» и не падает.
