nil-указатель и new
Что будет, если взять бумажку для адреса, но ничего на ней не написать? Пойти по такой бумажке некуда. В Go это и есть указатель без адреса. Указатель, объявленный через var, ещё ни на что не указывает. Программа с записью через него компилируется, но при запуске завершается паникой:
package main
func main() {
var p *int
*p = 5 // ✗
}panic: runtime error: invalid memory address or nil pointer dereferenceПервая строка паники называет причину: программа обратилась по недопустимому адресу, когда разыменовала nil-указатель. Почему компилятор эту ошибку не поймал?
- Он проверяет типы, а
*pимеет правильный типint. - Пустую бумажку программа замечает только во время выполнения.
Эта паника — одна из самых частых в реальных программах на Go.
Значение nil
Нулевое значение любого указателя — nil, та самая пустая бумажка. Её можно напечатать и сравнить с nil:
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: она создаёт новую переменную указанного типа и возвращает её адрес. Переменная получает нулевое значение своего типа:
p := new(int)
fmt.Println(*p) // 0
*p = 5
fmt.Println(*p, p != nil) // 5 trueПосмотрим, что здесь произошло:
new(int)построила дом, положила в него0и отдала бумажку с адресом.- Поэтому первый
Printlnпечатает0. *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 через разыменование:
p := new(int)
if *p == nil { // ✗
fmt.Println("no value")
}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 и подпишись на канал — откроются все уроки.