Перейти к содержимому
Теория · Указатели · урок 5 из 8

nil-указатель и new

Что будет, если взять бумажку для адреса, но ничего на ней не написать? Пойти по такой бумажке некуда. В Go это и есть указатель без адреса. Указатель, объявленный через var, ещё ни на что не указывает. Программа с записью через него компилируется, но при запуске завершается паникой:

GO
package main

func main() {
    var p *int
    *p = 5 // ✗
}
TEXT
panic: runtime error: invalid memory address or nil pointer dereference

Первая строка паники называет причину: программа обратилась по недопустимому адресу, когда разыменовала nil-указатель. Почему компилятор эту ошибку не поймал?

  • Он проверяет типы, а *p имеет правильный тип int.
  • Пустую бумажку программа замечает только во время выполнения.

Эта паника — одна из самых частых в реальных программах на Go.

Значение nil

Нулевое значение любого указателя — nil, та самая пустая бумажка. Её можно напечатать и сравнить с nil:

GO
var p *int
fmt.Println(p, p == nil) // <nil> true

Что можно и чего нельзя делать с nil-указателем:

  • печатать и сравнивать с nil можно, как в примере выше;
  • разыменовывать нельзя: чтение *p вызывает ту же панику, что и запись;
  • прочитать оттуда 0 тоже не получится: за nil нет никакого дома.

Здесь указатель отличается от map: отсутствующий ключ map читается как нулевое значение, а nil-указатель — нет.

Проверка перед разыменованием

Если указатель может оказаться nil, проверьте его перед разыменованием: if p != nil { fmt.Println(*p) }. Такая проверка нужна и функции с параметром-указателем: вызывающий код мог передать ей пустую бумажку.

Два способа получить адрес

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

Адрес существующей переменной

Оператор & берёт адрес у переменной, которая уже есть: p := &x. Бумажка ведёт к дому с именем x, поэтому до него можно добраться и по имени, и по бумажке. Запись *p = 5 видна через x.

Функция new

Можно попросить Go построить новый дом. Это делает встроенная функция new: она создаёт новую переменную указанного типа и возвращает её адрес. Переменная получает нулевое значение своего типа:

GO
p := new(int)
fmt.Println(*p) // 0
*p = 5
fmt.Println(*p, p != nil) // 5 true

Посмотрим, что здесь произошло:

  1. new(int) построила дом, положила в него 0 и отдала бумажку с адресом.
  2. Поэтому первый Println печатает 0.
  3. *p = 5 кладёт в дом 5, и второй Println печатает 5 true.

p := new(int) работает как пара строк x := 0 и p := &x. Разница одна: у нового дома нет своего имени, и добраться до него можно лишь по бумажке p. Поэтому new удобна, когда нужен указатель, а отдельное имя для переменной — нет.

Каждый вызов new строит отдельный дом. После a := new(int) и b := new(int) сравнение a == b даёт false, а *a == *b — true. Адреса у переменных разные, а значения одинаковые.

new и make

На собеседовании любят спрашивать, чем new отличается от make. Ответ из двух частей:

  • make создаёт срезы и map и возвращает готовое значение, а не указатель;
  • new подходит для любого типа и всегда возвращает указатель.

Частая ошибка

Указатель проверили на nil через разыменование:

GO
p := new(int)
if *p == nil { // ✗
    fmt.Println("no value")
}
TEXT
invalid operation: *p == nil (mismatched types int and untyped nil)

Программа не компилируется: *p — это число типа int, а число не бывает nil. Пустой бывает бумажка, а не дом, поэтому с nil сравнивают сам указатель: p == nil.

Главное

  • Нулевое значение указателя — nil: указатель без адреса.
  • Чтение или запись через nil-указатель компилируются, но вызывают панику при запуске.
  • new(T) создаёт переменную типа T с нулевым значением и возвращает её адрес; make создаёт срезы и map.
  • С nil сравнивают указатель (p == nil), а не *p.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом