Пока что значения в наших программах жили каждое в своей коробке. Передали int
в функцию — функция получила копию, поковырялась в ней, а ваша исходная
переменная осталась нетронутой. Часто это именно то, что нужно: меньше сюрпризов,
никто исподтишка не меняет ваши данные.
Но иногда копия мешает. Вы хотите, чтобы функция изменила вашу переменную,
а не её слепок: пополнила баланс счёта, отметила задачу выполненной. Или данные
большие, и копировать их на каждый вызов жалко. Для этого есть указатель —
значение, которое хранит не сами данные, а адрес, по которому они лежат в
памяти.
Возьмём функцию, которая «должна» удвоить число. Наивная попытка:
package mainimport "fmt"func doubleCopy(n int) { n = n * 2 // меняем локальную копию}func main() { x := 21 doubleCopy(x) fmt.Println("после doubleCopy:", x) // 21 — ничего не изменилось}
Функция получила копию x в параметре n. Удвоила копию. Вернулась — копия
исчезла, а x как был 21, так и остался. Это не баг, а правило Go: аргументы передаются по значению, то есть копированием. Чтобы
функция дотянулась до самой переменной вызывающего, ей нужен её адрес.
Указатель — это значение, чей тип записывается как *T (читается «указатель на
T»). Внутри лежит адрес памяти, по которому находится значение типа T.
С указателями работают два оператора:
&x — взять адрес переменной x. Результат имеет тип *T.
*p — разыменовать указатель p: добраться до значения, на которое он
показывает. Это работает и на чтение, и на запись.
Запомнить легко: & упаковывает переменную в адрес, * распаковывает адрес
обратно в переменную. Они обратны друг другу.
x := 10p := &x // p имеет тип *int, хранит адрес xfmt.Println(*p) // 10 — читаем значение по адресу*p = 20 // пишем по адресуfmt.Println(x) // 20 — изменили саму x, не копию
Теперь та же функция, но принимающая адрес. Внутри мы разыменовываем указатель и
пишем по нему — а значит, дотягиваемся до настоящей переменной main.
package mainimport "fmt"func double(n *int) { *n = *n * 2 // читаем по адресу, удваиваем, пишем обратно по адресу}func main() { x := 21 fmt.Println("до: ", x) double(&x) // передаём адрес x, а не саму x fmt.Println("после:", x) // 42 — изменилась исходная переменная}
Вывод:
до: 21после: 42
Обратите внимание: в double мы пишем *n = ..., а не n = .... Присвоить
самому n новое значение было бы бессмысленно — мы бы поменяли локальную копию
адреса. Нам нужно то, что по этому адресу, отсюда звёздочка.
Указатель — это ещё и способ дать двум кускам кода смотреть на одни и те же
данные. Пока существует адрес, можно читать и менять значение из разных мест, и
все увидят одну и ту же картину — копий-то нет.
package mainimport "fmt"func main() { score := 100 a := &score b := &score // оба указателя смотрят на одну переменную *a += 50 // меняем через a *b += 10 // меняем через b fmt.Println("score =", score) // 160 — обе правки попали в одну ячейку fmt.Println("через a:", *a, "через b:", *b)}
a и b — разные переменные-указатели, но внутри у них один и тот же адрес.
Поэтому правка через любой из них видна через другой и через саму score. Это
ровно то, что делает функция, принимающая *int: она получает доступ к ячейке
вызывающего.
Нулевое значение для любого указателя — nil. Это «адрес, которого нет»: такой
указатель ни на что не показывает. Объявленный, но не инициализированный
указатель равен nil:
var p *int // p == nilfmt.Println(p == nil) // true
Попытка разыменовать nil — *p, когда p == nil — это паника во время
выполнения (nil pointer dereference), и программа падает. Поэтому, если есть
сомнение, пришёл ли указатель «настоящим», проверяют его перед разыменованием:
package mainimport "fmt"func describe(p *int) { if p == nil { fmt.Println("указателя нет (nil)") return } fmt.Println("значение по адресу:", *p)}func main() { x := 7 describe(&x) // значение по адресу: 7 describe(nil) // указателя нет (nil)}
Без проверки программа упала бы с сообщением
panic: runtime error: invalid memory address or nil pointer dereference. Его
стоит запомнить в лицо: это одна из самых частых аварий в программах на Go.
nil-указатель сам по себе не ошибка; ошибка — попытаться достать значение
там, где его нет. nil часто используют осознанно: например, как «значение
отсутствует». Но за каждым разыменованием стоит молчаливое предположение, что
указатель не nil.
Чаще всего указатели в Go встречаются со структурами. Подробно структуры
разобраны в седьмой главе, а здесь хватит минимума.
Структура собирает несколько значений в одно: объявление
type Account struct { Owner string; Balance int } вводит новый тип «счёт» с
двумя полями, запись Account{Owner: "Ира", Balance: 100} создаёт значение
этого типа, а acc.Balance обращается к полю.
Указатель на структуру нужен по тем же двум причинам: чтобы функция меняла поля
исходной структуры и чтобы не копировать большую структуру на каждый вызов.
Приятная мелочь: чтобы добраться до поля через указатель, не нужно писать
(*p).Field. Go разрешает короткую форму p.Field — он сам разыменует указатель
за вас. Обе записи означают одно и то же, но короткая читается человечнее.
Если бы deposit принимал Account по значению, каждый вызов работал бы с
копией, и баланс в main остался бы 100. Указатель даёт функции доступ к
настоящему счёту.
Запись &Account{...} сразу создаёт структуру и берёт её адрес: получается
*Account без промежуточной переменной:
package mainimport "fmt"type Point struct{ X, Y int }func main() { p := &Point{X: 3, Y: 4} // p имеет тип *Point p.X = 10 // короткая форма разыменования fmt.Println(p.X, p.Y) // 10 4}
Если вы писали на C, следующий код вас насторожит: функция создаёт переменную и
возвращает её адрес. Переменная ведь «умрёт», когда функция завершится?
package mainimport "fmt"type Point struct{ X, Y int }func newPoint(x, y int) *Point { p := Point{X: x, Y: y} return &p // в Go это безопасно}func main() { a := newPoint(1, 2) a.X = 5 fmt.Println(*a)}
Вывод: {5 2}. В Go это безопасно и так пишут постоянно. Компилятор видит, что
адрес p уходит за пределы функции, и размещает переменную там, где она
проживёт сколько нужно. Сборщик мусора освободит её, когда на неё не останется
указателей. Думать о том, «где лежит» переменная, вам не нужно.
Ещё один способ получить указатель — встроенная функция new(T). Она создаёт
переменную типа T с нулевым значением и возвращает её адрес: n := new(int)
даёт *int, а *n равно 0. На практике чаще пишут &T{...}, но new
встретится в чужом коде.
Не нужно превращать всё подряд в указатели «на всякий случай». Простое правило:
Передавайте по указателю, если функция должна изменить аргумент так,
чтобы изменение увидел вызывающий.
Передавайте по указателю, если значение большое (крупная структура) и
копировать его на каждый вызов накладно.
Передавайте по значению во всех остальных случаях — для маленьких значений
(int, bool, небольшие структуры) это проще, безопаснее и часто не медленнее.
Отдельно держите в голове: срезы (slices), отображения (maps) и каналы уже внутри
себя содержат ссылку на общие данные. Передавать map по указателю обычно не
нужно — функция и так меняет тот же самый map. Но это тема следующих глав; здесь
важно лишь не удивляться, что не каждый «изменяемый» тип требует *.
Меняют сам указатель вместо значения по нему. Внутри функции n = &other
(где n имеет тип *int) меняет локальную копию адреса и не меняет указатель
вызывающего. Запись n = 42 вообще не компилируется: число не является указателем. Чтобы поменять данные, нужно *n = 42.
Разыменовывают nil.*p при p == nil — это паника. Если указатель
может быть «пустым», проверяйте if p != nil перед разыменованием.
Делают указатель там, где он не нужен.*int для функции, которая просто
читает число и ничего не меняет, только усложняет код и плодит лишние &/*.
Нужна мутация или большая структура — да; иначе по значению чище.
Сравнивают адреса вместо значений.p == q для двух указателей проверяет,
смотрят ли они на одну и ту же переменную, а не равны ли значения. Две разные
переменные со значением 5 дадут false; чтобы сравнить значения, пишите
*p == *q.
Напишите increase(p *int), которая прибавляет 3 к исходному числу. В main создайте x := 7, передайте &x и напечатайте x. Затем добавьте проверку, позволяющую безопасно вызвать increase(nil).
В следующей главе, «Срезы, карты, строки», указатели появятся неявно: срез внутри себя хранит адрес массива с данными. Это объяснит, почему функция, получившая копию среза, всё же может изменить его элементы. Затем структуры и методы покажут, как применять получатель-указатель к собственному типу. К модели памяти нескольких горутин перейдём уже в продвинутом курсе, после освоения обычной передачи данных.