Menu

Указатели в Golang: & и *, new, nil и когда их использовать

Указатель хранит адрес значения. Операторы & и *, new, указатели на структуры, почему можно безопасно вернуть указатель на локальную переменную, когда использовать указатели и паника при разыменовании nil.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

& и *

Указатель хранит адрес значения в памяти. &x даёт указатель на x. *p переходит по указателю к значению.

У звёздочки две роли. В типе (*int) она означает «указатель на int». В выражении (*p) она означает «значение, на которое указывает p». Если напечатать сам p, получится адрес вроде 0xc000012345, который меняется от запуска к запуску.

Зачем нужны указатели: общий доступ вместо копирования

Go передаёт всё по значению. Функция получает копию каждого аргумента, поэтому не может изменить переменную вызывающего кода. Передача указателя копирует адрес, и обе стороны добираются до одного и того же значения.

Ссылок в смысле C++ в Go нет. «Передача по ссылке» в Go означает передачу указателя по значению.

Указатели на структуры

Указатели на структуры это самые частые указатели в коде на Go. Обращение к полю разыменовывает автоматически: p.Name означает (*p).Name.

Методы подчиняются той же логике: получатель-указатель позволяет методу менять структуру. Выбор между получателем-значением и получателем-указателем разобран на странице о методах.

new

new(T) выделяет обнулённое значение T и возвращает его адрес. Это то же самое, что объявить переменную и взять её адрес.

p := new(int) // *int pointing at 0
var x int
q := &x           // same thing, two lines
a := new(Account) // same as &Account{}

На практике для структур чаще пишут &T{...}, потому что так можно задать поля в том же выражении. new удобен для указателей на базовые типы. Не путайте его с make, который создаёт слайсы, мапы и каналы и возвращает их готовыми к работе, а не указателями.

Возвращать указатель на локальную переменную безопасно

В C возврат адреса локальной переменной это баг. В Go это обычный код.

Где будет жить значение, в стеке или в куче, решает escape-анализ компилятора. Вы этого никогда не выбираете и никогда не освобождаете память: сборщик мусора забирает её, когда на неё больше ничего не указывает. go build -gcflags=-m печатает эти решения, если интересно.

nil-указатели

Нулевое значение любого типа указателя это nil. Разыменование nil вызывает панику.

Вывод:

1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference

Без recover программа упала бы с этим сообщением и трассировкой стека, начинающейся с panic: runtime error: invalid memory address or nil pointer dereference и [signal SIGSEGV: segmentation violation ...]. Типичные источники:

  • переменная-указатель или поле структуры, которым так и не присвоили значение
  • m[key] в map[string]*T для отсутствующего ключа, что возвращает nil
  • проигнорированная ошибка: f, _ := os.Open(path) оставляет f равным nil при неудаче
  • метод с получателем-указателем, вызванный на nil-указателе, который затем читает поле

Сам вызов метода на nil-указателе допустим. Паника случается, только когда метод обращается к полям получателя.

Когда использовать указатель

Используйте указатель, когда:

  • функция или метод должны изменить значение вызывающего кода
  • значение это большая структура, которую часто передают
  • тип нельзя копировать (он содержит sync.Mutex или представляет уникальный ресурс, например соединение)
  • нужно выразить «нет значения» через nil, например необязательное поле в JSON-структуре (*int отличает «отсутствует» от 0)

Не используйте указатель, когда:

  • значение маленькое и только для чтения (time.Time, Point)
  • тип и так ведёт себя как ссылка: слайсам, мапам, каналам, функциям и интерфейсам * перед ними нужен редко. *[]int или *map[string]int почти всегда ошибка

Указатели не быстрее автоматически. Копировать маленькую структуру дёшево, а указатель может вынудить выделение в куче и добавить работы сборщику мусора.

Указатели и переменные цикла

Брать адрес переменной цикла безопасно начиная с Go 1.22, потому что у каждой итерации своя переменная:

Программа печатает 0 1 2. До Go 1.22 (где также появился range 3) тот же цикл, записанный как for i := 0; i < 3; i++, печатал 3 3 3, потому что все три указателя делили одну переменную. Учтите, что &v в for _, v := range items указывает на копию элемента, а не на элемент слайса. Чтобы получить указатель на сам элемент, используйте &items[i].

Частые ошибки

  • Разыменование без проверки. Проверяйте if p == nil везде, где возможен nil.
  • Указатель на значение из range. &v указывает на копию; используйте &s[i].
  • Указатели на слайсы и мапы. Нужны редко. Лучше возвращайте новый слайс.
  • Хранение &s[i] при append. Если append перевыделит память, указатель останется на старом массиве, и записи через него не будут видны в новом слайсе.

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

Что означают & и * в Go?

&x берёт адрес x и даёт указатель типа *T. *p разыменовывает указатель: читает или записывает значение, на которое он указывает. В типе *T означает «указатель на T».

Чем new отличается от make в Go?

new(T) выделяет обнулённое значение T и возвращает *T; работает с любым типом. make работает только со слайсами, мапами и каналами и возвращает инициализированное значение этого типа (не обнулённое и не указатель): make(map[string]int) сразу готова к работе, а new(map[string]int) это указатель на nil-мапу.

Можно ли вернуть указатель на локальную переменную в Go?

Да. В отличие от C, это безопасно. Escape-анализ компилятора видит, что переменная переживёт функцию, и размещает её в куче. func newInt() *int { x := 5; return &x } это корректный код на Go.

Откуда берётся «invalid memory address or nil pointer dereference» в Go?

Это чтение или запись через nil-указатель: переменная *T, которой так и не присвоили значение, поиск отсутствующего ключа в map[K]*V или функция, вернувшая nil вместе с ошибкой, которую вы не проверили. Проверяйте на nil или инициализируйте указатель перед использованием.

Есть ли в Go арифметика указателей?

Нет. К указателю нельзя прибавить число или обратиться через него к памяти по индексу. Пакет unsafe разрешает это для низкоуровневого кода, без каких-либо гарантий безопасности. Обычный код на Go использует слайсы для непрерывной памяти.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ