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

any и type assertion в Go

any и извлечение конкретного типа

Положить что угодно в 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, остальные разбираются в тренажёре.

  1. #go_any_assert1 / 5
    var i any = "text". Что делает n := i.(int) и чем отличается n, ok := i.(int)?
    A)Оба вернут 0 и false: несовпадение типа тихо игнорируется
    B)Первое вернёт 0, второе тоже 0, но уже с ok=true
    C)Первое паникует (interface conversion), второе даёт 0, ok=false
    D)Оба не скомпилируются: ассертить 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.

  2. #go_any_assert2 / 5
    Зачем нужна ассерция к ИНТЕРФЕЙСУ, например v, ok := x.(io.Closer)?
    A)Узнать в рантайме, реализует ли значение это поведение, и вызвать его
    B)Преобразовать значение в другой конкретный тип по длинной цепочке
    C)Ускорить вызовы метода, минуя таблицу интерфейса
    D)Сравнить два интерфейсных значения на равенство их типов
    показать ответ и разбор
    +A)Узнать в рантайме, реализует ли значение это поведение, и вызвать его

    // разбор: Ассерция к интерфейсу x.(io.Closer) спрашивает: реализует ли динамический тип x этот интерфейс? Если да — ok=true и получаешь значение с этим поведением (можно вызвать Close). Классика: опционально закрыть ресурс, если он Closer; отдать расширенное поведение, если тип его поддерживает (например http.Flusher). Про наличие методов, а не про скорость.

  3. #go_any_assert3 / 5
    Что произойдёт при v := x.(int), если в x лежит строка?
    A)v получит строку, приведённую к int по правилам преобразования
    B)Произойдёт паника: ассерция без второго возвращаемого значения не прощает промах
    C)Компилятор отклонит код, обнаружив несоответствие типов заранее
    D)v получит нулевое значение int, программа продолжит работу
    показать ответ и разбор
    +B)Произойдёт паника: ассерция без второго возвращаемого значения не прощает промах

    // разбор: Однозначная форма ассерции требует совпадения типа и паникует при промахе. Безопасная — с двумя значениями: v, ok := x.(int), где ok сообщает об успехе, а v при неудаче нулевой. Компилятор тут не помощник: в интерфейсе тип известен только во время работы, поэтому в любом коде, где содержимое приходит извне, пишут форму с ok.

  4. #go_any_assert4 / 5
    Почему передача int в параметр типа any заставляет значение уехать в кучу?
    A)Интерфейс хранит пару «тип и указатель на значение», и значение нужно разместить по адресу
    B)Рантайм переносит в кучу все аргументы функций с переменным числом параметров
    C)Числа в Go размещаются в куче независимо от того, как они передаются
    D)Сборщик мусора требует стабильного адреса для значений в куче
    показать ответ и разбор
    +A)Интерфейс хранит пару «тип и указатель на значение», и значение нужно разместить по адресу

    // разбор: Переменная интерфейсного типа — два слова: дескриптор типа и указатель на данные. Чтобы указатель было куда направить, значение должно жить по адресу, переживающему вызов, — escape analysis отправляет его в кучу («x escapes to heap» в выводе -gcflags=-m). Отсюда цена fmt.Println в горячем цикле: каждый аргумент — аллокация и работа сборщику.

  5. #go_any_assert5 / 5
    В какое значение 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 без потери больших целых.

дальше

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

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