Типы и нулевые значения в 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, остальные разбираются в тренажёре.
- Что делает 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 для битовых флагов. Это не случайность и не про типизацию.
- Есть 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. Многословно, зато исключает целый класс ошибок с потерей точности и переполнением.
- Чем 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) — рабочую мапу.
- Чем 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.
- Что делают встроенные 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 удаляет все ключи, у слайса обнуляет элементы, не меняя длину. Всё это встроенные функции компилятора, импорта они не требуют.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.