any и type assertion в Go
Положить что угодно в any легко. Достать обратно - вот где начинается работа: компилятор больше не знает, что там лежит, и проверка переезжает во время исполнения. Форма записи при этом решает, получишь ты аккуратное «нет, не оно» или панику посреди обработки запроса.
Стержень: у утверждения типа две формы - с одним результатом и с двумя; в рабочем коде почти всегда нужна вторая.
// Формулировки: «как достать значение из any?», «что будет при неверном типе?», «почему число из JSON стало float64?»
Две формы, и разница драматична
Форма с одним результатом - v := i.(int) - при несовпадении типа паникует. Форма с двумя - v, ok := i.(int) - при несовпадении возвращает нулевое значение и false, не паникуя. Синтаксис отличается на одну переменную, поведение - на упавший процесс.
Первая форма уместна там, где несовпадение означает ошибку в собственном коде и падать правильно: сразу после того, как ты сам положил значение в any, или в месте, где инвариант гарантирован.
Вторая - везде, где значение пришло снаружи: из декодированного JSON, из чужого API, из кеша с интерфейсными значениями. Проверено на утверждении к интерфейсу, которого тип не реализует: ok равен false, никакой паники, дальше идёт нормальная обработка.
// Утверждать можно и к интерфейсу, а не только к конкретному типу: v, ok := x.(io.Closer) отвечает на вопрос «а умеет ли эта штука закрываться». Так в стандартной библиотеке проверяют дополнительные возможности значения, не требуя их от всех.
v := i.(int) // при несовпадении - паника
v, ok := i.(int) // при несовпадении - 0 и false
if c, ok := x.(io.Closer); ok {
defer c.Close() // умеет закрываться - закроем
}Цена укладки в интерфейс
Положить значение в интерфейс - не бесплатная операция. Интерфейс хранит указатель на значение, поэтому значение, которое раньше жило на стеке, обычно уезжает в кучу. Это видно в выводе компилятора с флагом -gcflags=-m строчкой про escapes to heap.
Самый массовый случай - аргументы fmt.Println и подобных функций: они принимают ...any, значит каждый аргумент укладывается в интерфейс и уходит в кучу. В горячем цикле это заметно на профиле аллокаций.
Отсюда правило: any хорош на границах, где типы действительно заранее неизвестны - декодирование, универсальные кеши, логирование. Внутри своей логики его лучше не тащить: теряется проверка компилятором и добавляется работа сборщику.
// С Go 1.18 у any появилась честная альтернатива для случая «любой тип, но один и тот же» - дженерики. Функция с параметром типа сохраняет типизацию и не требует утверждений на выходе.
JSON: почему число стало float64
Классическая история. Декодируешь JSON в map[string]any, лезешь за полем с числом, утверждаешь его к int - и получаешь панику или false. Причина в том, что в JSON нет отдельного целочисленного типа, и стандартный декодер укладывает любое число в float64.
Значит утверждать надо к float64 и приводить дальше самому. И тут же вылезает вторая беда: float64 точно представляет целые примерно до девяти квадриллионов, а большие идентификаторы за этой границей молча теряют младшие разряды. Идентификатор превращается в соседний, и ошибка находится в проде.
Лечится двумя способами. Первый и правильный - декодировать в структуру с нужными типами полей, а не в map[string]any. Второй, когда структура заранее неизвестна, - декодер с опцией UseNumber: тогда числа приходят типом json.Number, из которого можно достать и целое, и строку без потерь.
// Общее правило: map[string]any для JSON - крайняя мера. Структура даёт и типы, и валидацию имён полей, и документацию формата в одном месте.
- json.Number
- строковое представление числа из JSON; позволяет достать большое целое без потери точности
Как отвечать: «Как достать значение из any и что будет при неверном типе?»
Утверждением типа, и у него две формы. Короткая, v := i.(int), при несовпадении паникует - её беру только там, где несовпадение означает ошибку в моём собственном коде и падать правильно. Форма с двумя результатами, v, ok := i.(int), при несовпадении возвращает нулевое значение и false, ничего не роняя - её использую везде, где значение пришло снаружи: из JSON, из чужого API, из кеша. Утверждать можно и к интерфейсу, например к io.Closer, чтобы спросить «а умеет ли эта штука закрываться» - так в стандартной библиотеке проверяют дополнительные возможности. Отдельно назову самую частую ловушку: если декодировать JSON в map[string]any, все числа приходят как float64, потому что в JSON нет отдельного целого типа. Утверждение к int там всегда даст false, а большие идентификаторы ещё и потеряют точность. Правильное лечение - декодировать в структуру с нужными типами, а если структура заранее неизвестна, включать у декодера режим json.Number.
Обе формы названы с критерием выбора, показано утверждение к интерфейсу и разобрана самая частая практическая ловушка вместе с двумя способами её убрать.
На чём валят
- −Используют короткую форму утверждения для внешних данных и роняют процесс.
- −Утверждают число из JSON к int и удивляются, что там float64.
- −Теряют точность больших идентификаторов, прошедших через float64.
- −Тащат any вглубь логики вместо структур или дженериков.
- −Забывают, что укладка в интерфейс отправляет значение в кучу.
- −Не проверяют ok и работают с нулевым значением как с настоящим.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 6, остальные разбираются в тренажёре.
- var i any = "text". Что делает n := i.(int) и чем отличается n, ok := i.(int)?A)Оба вернут 0 и false: несовпадение типа тихо игнорируетсяB)Первое вернёт 0, второе тоже 0, но уже с ok=trueC)Первое паникует (interface conversion), второе даёт 0, ok=falseD)Оба не скомпилируются: ассертить int из строки запрещено статически
показать ответ и разбор
+C)Первое паникует (interface conversion), второе даёт 0, ok=false// разбор: type assertion в одиночной форме i.(int) паникует, если динамический тип не int: «interface conversion: interface {} is string, not int». Форма с запятой-ok n, ok := i.(int) не паникует — возвращает нулевое значение и ok=false при несовпадении. Поэтому в неуверенных местах берут comma-ok. Спот-чек подтвердил панику и 0/false.
- Зачем нужна ассерция к ИНТЕРФЕЙСУ, например v, ok := x.(io.Closer)?A)Узнать в рантайме, реализует ли значение это поведение, и вызвать егоB)Преобразовать значение в другой конкретный тип по длинной цепочкеC)Ускорить вызовы метода, минуя таблицу интерфейсаD)Сравнить два интерфейсных значения на равенство их типов
показать ответ и разбор
+A)Узнать в рантайме, реализует ли значение это поведение, и вызвать его// разбор: Ассерция к интерфейсу x.(io.Closer) спрашивает: реализует ли динамический тип x этот интерфейс? Если да — ok=true и получаешь значение с этим поведением (можно вызвать Close). Классика: опционально закрыть ресурс, если он Closer; отдать расширенное поведение, если тип его поддерживает (например http.Flusher). Про наличие методов, а не про скорость.
- Что произойдёт при v := x.(int), если в x лежит строка?A)v получит строку, приведённую к int по правилам преобразованияB)Произойдёт паника: ассерция без второго возвращаемого значения не прощает промахC)Компилятор отклонит код, обнаружив несоответствие типов заранееD)v получит нулевое значение int, программа продолжит работу
показать ответ и разбор
+B)Произойдёт паника: ассерция без второго возвращаемого значения не прощает промах// разбор: Однозначная форма ассерции требует совпадения типа и паникует при промахе. Безопасная — с двумя значениями: v, ok := x.(int), где ok сообщает об успехе, а v при неудаче нулевой. Компилятор тут не помощник: в интерфейсе тип известен только во время работы, поэтому в любом коде, где содержимое приходит извне, пишут форму с ok.
- Почему передача int в параметр типа any заставляет значение уехать в кучу?A)Интерфейс хранит пару «тип и указатель на значение», и значение нужно разместить по адресуB)Рантайм переносит в кучу все аргументы функций с переменным числом параметровC)Числа в Go размещаются в куче независимо от того, как они передаютсяD)Сборщик мусора требует стабильного адреса для значений в куче
показать ответ и разбор
+A)Интерфейс хранит пару «тип и указатель на значение», и значение нужно разместить по адресу// разбор: Переменная интерфейсного типа — два слова: дескриптор типа и указатель на данные. Чтобы указатель было куда направить, значение должно жить по адресу, переживающему вызов, — escape analysis отправляет его в кучу («x escapes to heap» в выводе -gcflags=-m). Отсюда цена fmt.Println в горячем цикле: каждый аргумент — аллокация и работа сборщику.
- В какое значение json.Unmarshal разберёт число, если приёмник объявлен как any?A)В int, если число целое, и в float64, если дробноеB)В float64 независимо от того, целое число или дробноеC)В json.Number, сохраняя исходную запись числаD)В string, чтобы не потерять точность при разборе
показать ответ и разбор
+B)В float64 независимо от того, целое число или дробное// разбор: Без конкретного типа кодировщик кладёт любое JSON-число в float64 — отсюда классическая паника на v.(int) после разбора в map[string]any. Лечится либо структурой с точными типами полей, либо decoder.UseNumber(), который отдаёт json.Number и позволяет разобрать значение в int64 без потери больших целых.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.