сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Интерфейсы и основы Go

Типы и нулевые значения в Go

Нулевые значения и строгие типы

var s string, var n int, var p *Foo - и ни одной строчки инициализации. В C на этих адресах лежал бы мусор, оставшийся от прошлого владельца памяти. В Go у всех трёх переменных определённые значения: пустая строка, ноль и nil. Это не мелочь ради удобства, а фундамент, на котором стоят типы, работающие сразу после объявления.

Стержень: объявленная переменная всегда имеет нулевое значение своего типа, а любое смешение типов требует явной конверсии.

// Формулировки: «чему равен var s string?», «почему int + float64 не компилируется?», «чем new отличается от make?»

Ноль есть у всех, и его делают рабочим

Память при выделении зануляется, поэтому неинициализированных переменных в Go просто не бывает. Числа получают 0, строки - пустую строку, булев тип - false. Указатели, слайсы, мапы, каналы, функции и интерфейсы получают nil.

Из этого выросла целая привычка проектирования: нулевое значение стараются сделать пригодным к работе. var mu sync.Mutex можно сразу блокировать, var sb strings.Builder - сразу писать, var wg sync.WaitGroup - сразу использовать. Конструктор им не нужен, и это считается признаком хорошего пакета: пользователю не приходится помнить про NewЧто-то.

Важное различие внутри самих nil-значений. В nil-слайс можно спокойно делать append - он вернёт слайс с одним элементом, проверено. Из nil-мапы можно читать - вернётся ноль типа значения, и len даст 0. А вот запись в nil-мапу паникует с текстом «assignment to entry in nil map». Разница в том, что append умеет создать массив с нуля, а мапе нужна заранее построенная хеш-таблица.

// Эта паника, кстати, обычная - её ловит recover. Дальше будет случай, когда рантайм убивает процесс без всякой возможности вмешаться, и разницу между двумя ситуациями любят спрашивать.

var ns []int
ns = append(ns, 1)   // [1] - в nil-слайс можно

var nm map[string]int
_ = nm["x"]          // 0 - читать nil-мапу можно
nm["x"] = 1          // panic: assignment to entry in nil map
нулевое значение
то, что лежит в переменной без инициализации: 0, пустая строка, false, nil
рабочий ноль
тип, пригодный к использованию сразу после var, без конструктора

Приведение только явное

Возьми переменную int и переменную float64 и попробуй их сложить - компилятор откажется. Нужно написать float64(i) + f, и это единственный способ. Молча расширять типы Go не будет никогда.

То же самое касается собственных типов. Объявил type Celsius float64 - и значение Celsius больше не подставляется туда, где ждут обычный float64, пока не приведёшь явно. Выглядит занудством ровно до того дня, когда кто-то попробует сложить Celsius с Fahrenheit: они оба float64 внутри, но компилятор не даст их перепутать.

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

// Исключение одно: нетипизированные константы. const x = 5 подставится и в int, и в float64, потому что тип у неё определяется в месте использования, а не в объявлении. Поэтому math.Pi можно умножать на что угодно без конверсий.

явная конверсия
T(v) - единственный способ сменить тип значения
нетипизированная константа
константа без объявленного типа; получает его в месте использования

new, make и встроенные функции

Две функции выделения памяти, и путают их постоянно. new(T) годится для любого типа: выделяет память под T, зануляет и возвращает *T. make работает ровно с тремя типами - слайс, мапа, канал - и возвращает не указатель, а готовое к работе значение.

Отсюда классическая ошибка: new(map[string]int) компилируется и возвращает указатель на nil-мапу. Разыменовал, попробовал записать - получил ту самую панику про nil map. Правильно тут только make(map[string]int).

С Go 1.21 в языке появились три встроенные функции, которые не надо импортировать: min и max берут крайнее из любого числа аргументов, clear опустошает мапу или обнуляет элементы слайса, не трогая его длину. Компилятор знает их сам.

// Тогда же, в 1.21, в стандартную библиотеку въехали пакеты slices и maps: сортировка, поиск, сравнение - обобщённо и без рефлексии, в отличие от старого sort.Slice. А в 1.23 к ним добавились итераторы, и стало можно писать slices.Sorted(maps.Keys(m)) одной строкой. Версии тут лучше не путать: на собеседовании легко назвать 1.21 там, где на самом деле 1.23.

make
создаёт готовый слайс, мапу или канал; только для этих трёх типов
clear
встроенная функция с Go 1.21: опустошает мапу, обнуляет элементы слайса

Как отвечать: «Чему равна неинициализированная переменная и почему нет неявных приведений?»

Неинициализированных переменных в Go нет вообще: любая объявленная получает нулевое значение своего типа - ноль у чисел, пустая строка, false, nil у указателей, слайсов, мап, каналов, функций и интерфейсов. Память при выделении зануляется, поэтому поведение детерминированное, мусора как в C не бывает. На этом свойстве строят библиотеки: sync.Mutex и strings.Builder готовы к работе сразу после var, конструктор им не нужен. Одна тонкость внутри самих nil-значений: в nil-слайс append работает, из nil-мапы читать можно, а вот запись в неё паникует - мапе нужна построенная хеш-таблица, а append умеет создать массив с нуля. Про приведения: Go принципиально не расширяет типы молча, int и float64 в одном выражении не складываются, нужно писать float64(i). Это же касается собственных типов на базе числа - Celsius и Fahrenheit нельзя перепутать, хотя внутри оба float64. Многословно, зато исчезает тихая потеря точности и путаница доменных величин. Единственное послабление - нетипизированные константы: они получают тип в месте использования.

Нулевое значение названо для всех групп типов, следствие для дизайна библиотек показано, разница между nil-слайсом и nil-мапой объяснена причиной, а строгость типов подана как осознанный размен - и отдельно названа граница правила.

На чём валят

  • Путают nil-слайс и nil-мапу: в первый append можно, запись во вторую паникует.
  • Ждут мусор в памяти как в C - в Go всё занулено детерминированно.
  • Пишут new(map[string]int) вместо make и получают указатель на nil-мапу.
  • Думают, что int сам расширится до float64 внутри выражения.
  • Забывают, что собственный тип на базе числа тоже требует явной конверсии.
  • Называют версию 1.21 для slices.Sorted и maps.Keys - итераторы приехали в 1.23.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 7, остальные разбираются в тренажёре.

  1. #go_types_zero1 / 5
    Что делает iota в блоке const?
    A)Вставляет случайное уникальное число в каждую константу блока
    B)Счётчик строк в const-блоке: 0 в первой, +1 в каждой следующей
    C)Импортирует значения перечисления из другого пакета в текущий
    D)Помечает константу нетипизированной, разрешая неявные преобразования
    показать ответ и разбор
    +B)Счётчик строк в const-блоке: 0 в первой, +1 в каждой следующей

    // разбор: iota — счётчик, привязанный к позиции строки в const-блоке: в первой строке он 0 и растёт на 1 с каждой следующей (сбрасывается в новом блоке). На нём строят перечисления: const (A = iota; B; C) даёт 0,1,2. Можно и выражения: 1 << iota для битовых флагов. Это не случайность и не про типизацию.

  2. #go_types_zero2 / 5
    Есть var i int и var f float64. Что даст выражение i + f?
    A)Ошибку компиляции: неявного приведения числовых типов в Go нет
    B)Значение float64: меньший тип автоматически расширяется до большего
    C)Значение int: дробная часть отбрасывается при сложении с целым
    D)Предупреждение компилятора и результат типа float64 во время работы
    показать ответ и разбор
    +A)Ошибку компиляции: неявного приведения числовых типов в Go нет

    // разбор: Go не приводит числовые типы молча: смешивать int и float64 в одном выражении нельзя, нужно писать float64(i) + f. Это распространяется и на собственные типы — значение type Celsius float64 не подставить туда, где ждут float64. Многословно, зато исключает целый класс ошибок с потерей точности и переполнением.

  3. #go_types_zero3 / 5
    Чем new(T) отличается от make(T, ...)?
    A)new создаёт значение в стеке, make — обязательно в куче приложения
    B)Это синонимы: обе выделяют память, различаются лишь формой записи
    C)new применяется к структурам и массивам, make — к числовым типам
    D)new даёт указатель на обнулённое значение, make инициализирует слайс, map или канал
    показать ответ и разбор
    +D)new даёт указатель на обнулённое значение, make инициализирует слайс, map или канал

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

  4. #go_types_zero4 / 5
    Чем byte отличается от rune при работе с текстом?
    A)byte — псевдоним uint8 и хранит один байт, rune — int32 и хранит код символа
    B)byte хранит символ ASCII, а rune — символ национального алфавита
    C)byte применяется в строках, а rune — исключительно в срезах и массивах
    D)Это один и тот же тип с разными именами, оставленными для читаемости кода
    показать ответ и разбор
    +A)byte — псевдоним uint8 и хранит один байт, rune — int32 и хранит код символа

    // разбор: Строка в Go — последовательность байтов в UTF-8, поэтому len даёт число байтов, а не символов: у «café» это 5. Индексация s[i] отдаёт byte, а range по строке идёт по code point и выдаёт rune вместе со смещением. Отсюда практика: режете по символам — переводите в []rune, работаете с сырьём — держите []byte.

  5. #go_types_zero5 / 5
    Что делают встроенные min, max и clear, появившиеся в Go 1.21?
    A)min и max сравнивают только целые, clear освобождает память под слайсом
    B)Все три работают лишь с дженерик-типами и требуют параметра типа
    C)min и max берут наименьшее и наибольшее из аргументов, clear опустошает map или обнуляет слайс
    D)min и max ограничивают значение диапазоном, clear удаляет неиспользуемые ключи
    показать ответ и разбор
    +C)min и max берут наименьшее и наибольшее из аргументов, clear опустошает map или обнуляет слайс

    // разбор: min и max принимают два и больше упорядочиваемых аргумента и возвращают крайнее значение — раньше это писали руками или тащили из библиотек. clear ведёт себя по-разному: у map удаляет все ключи, у слайса обнуляет элементы, не меняя длину. Всё это встроенные функции компилятора, импорта они не требуют.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.