Пока что значения в наших программах жили каждое в своей коробке. Передали 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)}
nil-указатель — это не ошибка сам по себе; ошибка — попытаться достать значение
там, где его нет. nil часто используют осознанно: например, как «значение
отсутствует». Но за каждым разыменованием стоит молчаливое предположение, что
указатель не nil.
Чаще всего указатели в Go встречаются именно со структурами. Причин две: хочется,
чтобы функция меняла поля исходной структуры, и не хочется копировать большую
структуру на каждый вызов.
Приятная мелочь: чтобы добраться до поля через указатель, не нужно писать
(*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}
Не нужно превращать всё подряд в указатели «на всякий случай». Простое правило:
Передавайте по указателю, если функция должна изменить аргумент так,
чтобы изменение увидел вызывающий.
Передавайте по указателю, если значение большое (крупная структура) и
копировать его на каждый вызов накладно.
Передавайте по значению во всех остальных случаях — для маленьких значений
(int, bool, небольшие структуры) это проще, безопаснее и часто не медленнее.
Отдельно держите в голове: срезы (slices), отображения (maps) и каналы уже внутри
себя содержат ссылку на общие данные. Передавать map по указателю обычно не
нужно — функция и так меняет тот же самый map. Но это тема следующих глав; здесь
важно лишь не удивляться, что не каждый «изменяемый» тип требует *.
Меняют сам указатель вместо значения по нему. Внутри функции n = 42
(где n имеет тип *int) перезаписывает локальную копию адреса и ничего не
даёт вызывающему. Чтобы поменять данные, нужно *n = 42.
Разыменовывают nil.*p при p == nil — это паника. Если указатель
может быть «пустым», проверяйте if p != nil перед разыменованием.
Делают указатель там, где он не нужен.*int для функции, которая просто
читает число и ничего не меняет, только усложняет код и плодит лишние &/*.
Нужна мутация или большая структура — да; иначе по значению чище.
Ждут стабильного адреса в выводе. Печатать сам адрес через %p ради
обучения смысла мало: конкретное число непредсказуемо и меняется от запуска к
запуску. Показывайте и проверяйте эффект мутации, а не голый адрес.
Указатели — это фундамент, на котором стоит многое в Go: методы с
получателем-указателем, изменяемые структуры, передача больших данных без копий.
Дальше вы увидите, как они работают вместе со структурами и
методами, и почему срезы и карты «изменяются сами» без
явных * — про это в главе про коллекции. А если
захочется понять, что вообще происходит с памятью под капотом, загляните в главу
про модель памяти — это уже более глубокий материал из
продвинутого курса по конкурентности.