В этом посте разберём, что такое нулевое значение (zero values), и почему на него можно смело рассчитывать как на разумное значение по умолчанию для только что созданных переменных.
Важно знать, какие нулевые значения имеют базовые типы в Go — это помогает правильно работать с ними в коде. Поэтому ниже мы приведём таблицу/список самых важных zero values.
Наконец, мы рассмотрим несколько примеров использования нулевых значений, которые позволяют нам писать меньше кода, полагаясь на компилятор, который устанавливает для нас разумные значения по умолчанию.

Что такое нулевое значение?

Когда переменная объявляется без явного присвоения начального значения, компилятор Go автоматически присваивает ей нулевое значение типа.
Это гарантирует, что каждая переменная имеет действительное значение, независимо от того, была ли она задана программистом или нет.
Например, каждый целочисленный тип имеет нулевое значение равное нулю и, пожалуй, неудивительно, что именно отсюда происходит название этого значения. Его с тем же успехом можно было бы назвать просто значением по умолчанию.
Посмотрим на приведенный ниже пример кода:
Мы не присвоили явное значение переменной number, но компилятор автоматически установил для нее значение 0, как вы увидите, если запустите код.
В fmt.Printf с помощью %v значение выводится в формате по умолчанию, но поскольку мы точно знали, что number — это целое число, можно было спокойно использовать %d, который специально предназначен для целых чисел.

Список нулевых значений для каждого типа данных

Ниже приведен список, содержащий нулевые значения для всех основных типов данных в Go:
  • int, int8, int16, int32, int64 - 0
  • uint, uint8, uint16, uint32, uint64 - 0
  • byte, rune - 0
  • float32, float64 - 0.0
  • string - ""
  • bool - false
  • chan, interface, map, func - nil
  • pointer, slice, array - nil
Большинство нулевых значений являются очевидными значениями по умолчанию. Например, для целых чисел имеет смысл начинать с нуля. Также имеет смысл инициализировать строки как пустые, а булевы переменные как false.
Однако, как видно из приведенной ниже таблицы, существует также много типов данных, нулевое значение которых равно nil. Эти типы, как правило, необходимо явно инициализировать каким-либо образом, прежде чем их можно использовать.
Следующая цитата из официальной документации Go нам говорит:
При выделении памяти под значение (через объявление переменной, вызов new или make) без явной инициализации, Go автоматически заполняет эту память значениями по умолчанию

Зачем и как использовать нулевые значения

Когда вы рассчитываете на значение по умолчанию для переменной, то подумайте, подойдёт ли для этого её нулевое значение.
Если да, то вы можете сэкономить себе работу, позволив компилятору Go инициализировать переменную, вместо того чтобы делать это явно самостоятельно.
В приведенном ниже коде нулевые значения используются в качестве неявных значений по умолчанию:
В приведенном выше простом примере мы просматриваем текстовую строку, чтобы проверить, содержит ли она буквы E и Z.
Если строка содержит букву E, то переменная containsE будет установлена в значение true. Аналогично, если строка содержит букву Z, то переменная containsZ будет установлена в значение true.
Эти переменные задаются независимо друг от друга, поэтому они могут быть как истинными, так и ложными, или находиться в совершенно разных состояниях, в зависимости от содержания нашей текстовой строки.
Если строка не содержит ни одной из искомых букв, то обе переменные будут установлены в false, даже если мы явно не присвоили этим переменным начальное значение. Мы просто используем нулевые значения по умолчанию.

Работа с нулевым значением в слайсах

Как видно из таблицы выше, нулевое значение для слайсов - это nil. Но при этом слайс в состоянии nil можно сразу использовать: когда вы добавляете элементы через append, Go сам выделит нужную память. То есть нулевое значение среза готово к работе без всякой инициализации.
В примере ниже показано использование слайса сразу после его объявления (без явной инициализации):
Вы можете видеть, что мы начинаем с объявления среза строк с именем words. Его значение сразу после объявления равно nil, но как только мы добавляем значения, срезу выделяется место в памяти, достаточное для хранения двух строк, которые мы к нему добавляем.
Длина среза words будет равна 4, а ёмкость будет больше или равна 4, поскольку компилятор Go может выделить больше памяти, чем требуется в данный момент. При этом Go никогда не откажется выполнить append только потому, что в срезе недостаточно места: он просто выделит дополнительную память (если, конечно, операционная система не вернёт ошибку из-за нехватки RAM, например, когда вся оперативная память машины уже исчерпана).
Наконец, мы соединяем строки в нашем фрагменте, объединяя их в одну длинную строку, которую выводим на консоль.

Неявная инициализация нулевого значения для сложных типов

Более сложные типы, такие как, например, структуры в стандартной библиотеке Go, также имеют нулевые значения.
Иногда эти типы необходимо явно инициализировать каким-либо способом, например, вызовом функции. Однако, как правило, лучше, если типы готовы к использованию сразу после объявления.
Код ниже содержит пример именно такого случая:
Мы начинаем с объявления переменной buffer, которая может содержать произвольную длину байтов. Затем мы записываем в нее некоторый текст в виде байтового фрагмента.
Нам не нужно было выполнять явную инициализацию, поскольку нулевое значение bytes.Buffer инициализируется по умолчанию.
Наконец, мы копируем buffer в os.Stdout, что является относительно низкоуровневым способом записи текста в консоль.
Мы могли бы использовать fmt.Print(buffer.String()), что, возможно, было бы более привычным способом решить ту же задачу, однако это привело бы к предварительному преобразованию байтов буфера в строку, а не к прямому выводу байтов.

Сложные типы, которым требуется явная инициализация

Если вы объявляете собственный экспортируемый тип в своём пакете, старайтесь сделать так, чтобы его нулевое значение было пригодно к использованию. Это сильно упростит жизнь всем, кто будет работать с вашим кодом. Тем не менее, эта достойная цель не всегда оказывается достижимой на практике.
Иногда переменная должна быть явно инициализирована каким-либо образом, прежде чем ее можно будет использовать, например, путем вызова функции или метода и передачи некоторых параметров для настройки.
В приведенном ниже коде показан пример из стандартной библиотеки Go:
Мы вызываем внешнюю программу, в данном случае встроенную команду Linux Bash echo, чтобы отобразить сообщение в консоли.
Переменную cmd всё равно придётся создать, она будет хранить данные о внешней команде. Но чтобы заполнить её нужной информацией, обязательно нужно вызвать exec.Command.
Первый аргумент exec.Command — это имя внешней команды, которую мы хотим вызвать. Последующие аргументы передаются самой команде. Мы знаем, что echo соединяет все переданные ему строки одним пробелом перед их отображением.
Вызов метода Output фактически запускает команду и возвращает ее стандартный вывод в виде слайса байтов.
Если бы мы не вызвали exec.Command и оставили cmd с нулевым значением, то cmd.Output() упал бы в панику с ошибкой exec: no command - ведь в переменной не было бы никакой команды для выполнения.
Обратите внимание, что мы могли бы упростить задачу, объявив и инициализировав переменную cmd в одной строке, например так:
Конечно, это был бы более правильный способ, но мы специально раздели эти два шага, чтобы наглядно продемострировать, то что теперь уже нельзя полагаться на нулевое значение переменной.