Все главы учебника
Содержание учебника

Указатели по-человечески

12 мин чтенияКонтент v0.12.0

О чём глава

Пока что значения в наших программах жили каждое в своей коробке. Передали int в функцию — функция получила копию, поковырялась в ней, а ваша исходная переменная осталась нетронутой. Часто это именно то, что нужно: меньше сюрпризов, никто исподтишка не меняет ваши данные.

Но иногда копия мешает. Вы хотите, чтобы функция изменила вашу переменную, а не её слепок: пополнила баланс счёта, отметила задачу выполненной. Или данные большие, и копировать их на каждый вызов жалко. Для этого есть указатель — значение, которое хранит не сами данные, а адрес, по которому они лежат в памяти.

Сначала — почему копия мешает

Возьмём функцию, которая «должна» удвоить число. Наивная попытка:

package main
 
import "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 := 10
p := &x      // p имеет тип *int, хранит адрес x
fmt.Println(*p) // 10 — читаем значение по адресу
*p = 20      // пишем по адресу
fmt.Println(x)  // 20 — изменили саму x, не копию

Мутация вызывающего: чиним удвоение

Теперь та же функция, но принимающая адрес. Внутри мы разыменовываем указатель и пишем по нему — а значит, дотягиваемся до настоящей переменной main.

package main
 
import "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 main
 
import "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. Это «адрес, которого нет»: такой указатель ни на что не показывает. Объявленный, но не инициализированный указатель равен nil:

var p *int       // p == nil
fmt.Println(p == nil) // true

Попытка разыменовать nil — *p, когда p == nil — это паника во время выполнения (nil pointer dereference), и программа падает. Поэтому, если есть сомнение, пришёл ли указатель «настоящим», проверяют его перед разыменованием:

package main
 
import "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 — он сам разыменует указатель за вас. Обе записи означают одно и то же, но короткая читается человечнее.

package main
 
import "fmt"
 
type Account struct {
	Owner   string
	Balance int
}
 
func deposit(a *Account, amount int) {
	a.Balance += amount // то же, что (*a).Balance += amount
}
 
func main() {
	acc := Account{Owner: "Ира", Balance: 100}
	deposit(&acc, 250)
	deposit(&acc, 50)
	fmt.Printf("%s: %d\n", acc.Owner, acc.Balance) // Ира: 400
}

Если бы deposit принимал Account по значению, каждый вызов работал бы с копией, и баланс в main остался бы 100. Указатель даёт функции доступ к настоящему счёту.

Запись &Account{...} сразу создаёт структуру и берёт её адрес: получается *Account без промежуточной переменной:

package main
 
import "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 main
 
import "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).

Проверка

После первого вызова x равен 10; вызов с nil ничего не меняет и не вызывает панику:

func increase(p *int) {
    if p == nil {
        return
    }
    *p += 3
}

&x передаёт адрес, *p обращается к значению по адресу. Сам p тоже копируется при вызове. Присвоение другого адреса локальному p не изменило бы x.

Что дальше

В следующей главе, «Срезы, карты, строки», указатели появятся неявно: срез внутри себя хранит адрес массива с данными. Это объяснит, почему функция, получившая копию среза, всё же может изменить его элементы. Затем структуры и методы покажут, как применять получатель-указатель к собственному типу. К модели памяти нескольких горутин перейдём уже в продвинутом курсе, после освоения обычной передачи данных.

Продолжить

Остались вопросы?