Слово nil
является существительным, что значит ничего или ноль. В программировании на Go nil
является нулевым значением. Как помните, переменная integer
, объявленная без значения, по умолчанию будет равна 0. Пустая строка будет нулевым значением переменной string
и так далее. Указатель, который ни на что не указывает, принимает значение nil
. Идентификатор nil
также обладает нулевым значением для срезов, карт и интерфейсов.
После изучения данной статьи вы сможете:
Рекомендуем вам супер TELEGRAM канал по Golang где собраны все материалы для качественного изучения языка. Удивите всех своими знаниями на собеседовании! 😎
Мы публикуем в паблике ВК и Telegram качественные обучающие материалы для быстрого изучения Go. Подпишитесь на нас в ВК и в Telegram. Поддержите сообщество Go программистов.
- Сделать что-то из ничего;
- Разобраться с проблемами в
nil
; - Увидеть, как Go усовершенствовал
nil
.
Содержание статьи
- Вызывает ли nil сбои в Golang?
- Защита методов в Golang
- Значения функций nil
- Срезы nil в Golang
- Карты nil Golang
- Интерфейсы nil в Go
- Альтернатива nil в Golang
Многие языки программирования также используют концепт nil. Среди его других названий — NULL
, null
или None
. В 2009 году перед релизом Go проектировщик языков программирования Тони Хоар выступил с презентацией под названием «Null References: The Billion Dollar Mistake«. В своей речи Хоар утверждал, что он ответственен за изобретение отсылки null в 1965 году. Он также говорил о том, что указатели в никуда были не лучшей идеей.
На заметку: В 1978 Тони Хоар также впервые описал принцип взаимодействующих последовательных процессов, или «communicating sequential processes», CSP. Его идеи лежат в основе конкурентности Go.
В Go nil является более дружелюбен и менее распространен, нежели в других языках программирования, однако и здесь нужно быть готовым к некоторым проблемам. Nil можно использовать не только по его прямому назначению, о чем говорит Франчес Кампой в своей презентации на GopherCon 2016.
Представьте созвездие, где каждая звезда указывает на ближайшую к ней соседнюю звезду. После вычислений, каждая звезда будет куда-то указывать, и нахождение ближайшей звезды становится быстрым разыменованием указателя.
Однако, пока вычисления не закончены, куда должны указывать указатели? Это тот случай, когда пригодится nil. Nil может значить ближайшую звезду, пока реальная звезда не будет найдена.
В какой еще ситуации указатель в никуда может быть полезен?
Вызывает ли nil сбои в Golang?
Если указатель никуда не указывает, попытка разыменования указателя не сработает, что показано в Листинге 1. Разыменование указателя nil приведет к сбою программы. Обычно пользователям такое совсем не нравится.
Я называю это моей ошибкой в миллиард долларов.
Тони Хоар
1 2 3 |
var nowhere *int fmt.Println(nowhere) // Выводит: <nil> fmt.Println(*nowhere) // Выводит: nil pointer dereference |
Избежать сбоя несложно. Это вопрос защиты от разыменования указателя nil с оператором if, что показано в следующем листинге.
1 2 3 4 5 |
var nowhere *int if nowhere != nil { fmt.Println(*nowhere) } |
По правде говоря, сбой в программе может стать следствием многих причин, не только разыменования указателя nil. К примеру, деление на ноль также приведет к сбою, и решение проблемы будет схожим. Даже так, подумайте обо всех программах, написанных в течение последних 50 лет, количество случайных разыменований указателя nil должен быть весьма значительным.
Существование nil обременяет программиста необходимостью принятия дополнительных решений. Должен ли код проверять наличие nil, и если да, то где? Что код должен делать, если какое-то значение равно nil? После всего вышесказанного, так уж ли плох nil?
Вовсе не обязательно постоянно пытаться избегать nil. По правде говоря, в некоторых случаях nil весьма полезен. В дополнение ко всему указатели nil в Go не так популярны, как указатели null в некоторых других языках, и есть способы избежать их использования в случае нужды.
Вопрос для проверки:
Каким будет нулевое значения типа *string
?
Защита методов в Golang
Методы регулярно получают указатели на структуры, что значит приемник может быть nil, как показано в примере ниже. Происходит ли разыменование указателя явно (*p
) или неявно через получение доступа к полю структуры (p.age
), значение nil вызовет сбой.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
type person struct { age int } func (p *person) birthday() { p.age++ // разыменование указателя nil } func main() { var nobody *person fmt.Println(nobody) // Выводит: <nil> nobody.birthday() } |
Скорее всего, сбой вызван после выполнения строки p.age++
. Удалите данную строку, тогда программа запустится.
На заметку: Сравните это с аналогичной программой в Java, где приемник null приведет к сбою программы сразу после вызова метода.
Go вызывает методы даже в том случае, если у приемника значение nil. Приемник nil ведет себя так же, как и параметр nil. Это значит, что методы могут защищать от значений nil, как показано в следующем примере.
1 2 3 4 5 6 |
func (p *person) birthday() { if p == nil { return } p.age++ } |
Вместо проверки на наличие nil
перед вызовом метода birthday
предыдущий листинг защищает от приемников nil внутри метода.
На заметку: В Objective-C автоматический запуск метода для nil не приводит к сбою, но при вызове метода будет возвращаться нулевое значение.
С тем, как управлять nil
в Go, разобрались. Методы могут возвращать нулевые значения, возвращать ошибки или приводить к сбою.
Вопрос для проверки:
Что делает доступ к полю (p.age)
, если p
является nil?
Значения функций nil в Golang
Когда переменная объявляется как тип функции, ее значение по умолчанию будет nil
. В следующем листинге у fn
тип функции, но нет присваивания к какой-то конкретной функции.
1 2 |
var fn func(a, b int) int fmt.Println(fn == nil) // Выводит: true |
Если бы предыдущий код вызывал fn(1, 2)
, программа привела бы к сбою с разыменованием указателя nil
, потому что нет функции, присвоенной fn
.
Можно проверить, является ли значение функции nil
, и предоставить поведение по умолчанию. В следующем листинге sort.Slice
используется для сортировки среза строк с функцией первого класса less
. Если nil
передается аргументу less
он приводится по умолчанию к функции для сортировки в алфавитном порядке.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package main import ( "fmt" "sort" ) func sortStrings(s []string, less func(i, j int) bool) { if less == nil { less = func(i, j int) bool { return s[i] < s[j] } } sort.Slice(s, less) } func main() { food := []string{"onion", "carrot", "celery"} sortStrings(food, nil) fmt.Println(food) // Выводит: [carrot celery onion] } |
Задание для проверки:
Напишите одну строку кода для сортировки food
от самой короткой до самой длинной строки Листинга 6.
Срезы nil в Golang
Срез, объявленный без композитного литерала или встроенной функции make, будет со значением nil
. К счастью, ключевое слово range
, а также встроенные функции len
и append будут работать со срезами nil, как показано в следующем примере.
1 2 3 4 5 6 7 8 9 10 11 |
var soup []string fmt.Println(soup == nil) // Выводит: true for _, ingredient := range soup { fmt.Println(ingredient) } fmt.Println(len(soup)) // Выводит: 0 soup = append(soup, "onion", "carrot", "celery") fmt.Println(soup) // Выводит: [onion carrot celery] |
Пустой срез и срез nil не эквивалентны, но зачастую они заменяют друг друга. Следующий пример передает nil
для функции, что принимает срез, пропуская шаг создания пустого среза.
1 2 3 4 5 6 7 8 |
func main() { soup := mirepoix(nil) fmt.Println(soup) // Выводит: [onion carrot celery] } func mirepoix(ingredients []string) []string { return append(ingredients, "onion", "carrot", "celery") } |
Если вы пишите функцию, что принимает срез, убедитесь, что срез nil ведет себя так же, как и пустой срез.
Вопрос для проверки:
Какие действия можно безопасно осуществлять над срезом nil?
Карты nil в Golang
Как и срезы, карты объявленные без композитного литерала или встроенной функции make
, обладают значением nil
. Карты можно прочитать даже когда у них значение nil, что показано в следующем листинге, хотя запись в карту nil вызовет сбой.
1 2 3 4 5 6 7 8 9 10 11 |
var soup map[string]int fmt.Println(soup == nil) // Выводит: true measurement, ok := soup["onion"] if ok { fmt.Println(measurement) } for ingredient, measurement := range soup { fmt.Println(ingredient, measurement) } |
Если функция только читает из карты, можно передавать nil
функции вместо создания пустой карты.
Вопрос для проверки:
Какие действия на картой nil приведут к сбою?
Интерфейсы nil в Go
Когда объявляется переменная типа интерфейса без присваивания, нулевым значением является nil
. Следующий листинг показывает, что тип интерфейса и значение являются nil
, и переменная считается равной nil
.
1 2 |
var v interface{} fmt.Printf("%T %v %v\n", v, v, v == nil) // Выводит: <nil> <nil> true |
Когда переменной с типом интерфейса присваивается значение, интерфейс внутренне указывает на тип и значение данной переменной. Это приводит к довольно удивительному поведению значения nil, что не считается равным nil
. Оба, тип интерфейса и значение должны быть типа nil для того чтобы переменная была равна nil
, что показано в следующем примере:
1 2 3 |
var p *int v = p fmt.Printf("%T %v %v\n", v, v, v == nil) // Выводит: *int <nil> false |
Специальный символ %#v
нужен для того, чтобы увидеть тип и значение, а также отображения того, что переменная содержит (*int)(nil)
, а не просто <nil>
, как показано в Листинге 12.
1 |
fmt.Printf("%#v\n", v) // Выводит: (*int)(nil) |
Во избежание неприятных сюрпризов при сравнении интерфейсов с nil лучше явно использовать идентификатор nil
вместо указания на переменную, что содержит nil.
Вопрос для проверки:
Каким будет значение s
при объявлении как var s fmt.Stringer
?
Альтернатива nil в Golang
Если значения нет, использование nil может показаться заманчивым. К примеру, указатель на целое число (*int
) может представлять как ноль, так и nil. Указатели нужны для указания, поэтому использование указателя просто для предоставления значение nil
не является лучшим вариантом.
Вместо использования указателя можно объявить небольшую структуру с несколькими методами. Это требует большего количества кода, но не запрашивает указатель или nil, как показано в следующем листинге.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
type number struct { value int valid bool } func newNumber(v int) number { return number{value: v, valid: true} } func (n number) String() string { if !n.valid { return "not set" } return fmt.Sprintf("%d", n.value) } func main() { n := newNumber(42) fmt.Println(n) // Выводит: 42 e := number{} fmt.Println(e) // Выводит: not set } |
Вопрос для проверки:
В чем преимущество подхода, используемого в Листинге 13?
Заключение
- Разыменование указателей nil приведет к сбою программы;
- Методы могут защитить от получения значений
nil
; - Поведение по умолчанию может быть предоставлено функциям, передаваемых в виде аргументов;
- Срез nil часто можно заменить пустым срезом;
- Карту nil можно читать, но записывать в нее нельзя;
- Если интерфейс выглядит так, будто это nil, убедитесь, что оба тип и значение являются
nil
; - nil — не единственный способ представления пустого значения.
Итоговое задание для проверки:
Рыцарь встал на пути Артура. Герой безоружен, он представлен значением nil
для leftHand *item
. Имплементируйте структуру character
с методами вроде pickup(i *item)
и give(to *character)
. Потом используйте выученное в данном уроке для написания скрипта, в котором Артур достает объект и передает его рыцарю. Каждое действие должно отображаться с соответствующим описанием.
Администрирую данный сайт с целью распространения как можно большего объема обучающего материала для языка программирования Go. В IT с 2008 года, с тех пор изучаю и применяю интересующие меня технологии. Проявляю огромный интерес к машинному обучению и анализу данных.
E-mail: vasile.buldumac@ati.utm.md
Образование
Технический Университет Молдовы (utm.md), Факультет Вычислительной Техники, Информатики и Микроэлектроники
- 2014 — 2018 Universitatea Tehnică a Moldovei, ИТ-Инженер. Тема дипломной работы «Автоматизация покупки и продажи криптовалюты используя технический анализ»
- 2018 — 2020 Universitatea Tehnică a Moldovei, Магистр, Магистерская диссертация «Идентификация человека в киберпространстве по фотографии лица»