Type switch в Go
Когда вариантов больше двух, цепочка утверждений с ok превращается в лестницу из вложенных if. Для этого есть отдельная конструкция - переключатель по типу, где каждая ветка проверяет тип и сразу даёт переменную нужного типа.
Стержень: в каждой ветке переменная имеет тип этой ветки; ветки проверяются сверху вниз, а case nil ловит пустой интерфейс.
// Формулировки: «какого типа переменная в ветке?», «что ловит case nil?», «когда разбор по типам оправдан?»
Как устроен разбор
Пишется как switch v := x.(type), и главное здесь - переменная v. В каждой ветке она имеет ровно тот тип, который в этой ветке указан: в ветке с Sq это Sq со всеми полями, в ветке со строкой - строка со всеми методами.
Проверено на фигуре: в ветке case Sq поле стороны читается напрямую, без дополнительного утверждения. Именно это отличает конструкцию от цепочки if с утверждениями - там пришлось бы объявлять переменную в каждой ветке отдельно.
Одна тонкость: если в одной ветке перечислено несколько типов через запятую, переменная в ней имеет исходный интерфейсный тип, а не конкретный. Логично - какой из перечисленных компилятору выбрать? Поэтому объединяют только те случаи, где конкретный тип не нужен.
// В ветке default переменная тоже остаётся интерфейсной. Это нормальное место, чтобы вернуть ошибку «неподдерживаемый тип» с текстом через %T - он печатает динамический тип и здорово помогает при отладке.
switch v := x.(type) {
case Sq:
fmt.Println("квадрат со стороной", v.S) // v это Sq
case string:
fmt.Println("строка длиной", len(v)) // v это string
case int, int64:
fmt.Println("целое, но тип не сужен:", v) // v остался any
case nil:
fmt.Println("в интерфейсе пусто")
default:
return fmt.Errorf("неподдерживаемый тип %T", v)
}Порядок веток и case nil
Ветки проверяются сверху вниз, и срабатывает первая подходящая. Обычно это неважно, потому что типы не пересекаются, но с интерфейсами пересечение бывает: значение может удовлетворять и io.Reader, и io.ReadCloser. Тогда порядок решает, и более узкий интерфейс ставят выше более широкого.
case nil срабатывает, когда интерфейс пуст - оба его слова нулевые. И тут важно не перепутать: он не ловит интерфейс, внутри которого лежит nil-указатель конкретного типа. Такое значение попадёт в ветку своего типа, потому что тип в паре записан.
Это ровно та же граница, что и в ловушке typed nil, только с другой стороны. Отсутствие ветки case nil при этом не ошибка - пустой интерфейс просто уедет в default.
// Если разбираешь ошибки, помни, что переключатель по типу не ходит по цепочке обёрток. Для ошибок правильный инструмент - errors.As, который развернёт все слои; разбор по типу увидит только верхнюю обёртку.
Где это уместно, а где пахнет
Три законных применения. Разбор данных с внешней границы, где типы заранее неизвестны: декодированный JSON, значения из очереди, аргументы шаблонизатора. Обработка нескольких форматов представления в одной функции. И реализация чего-то вроде функции печати, которой по определению приходит что угодно.
А вот разбор по типам собственных доменных типов - обычно признак того, что не хватает метода. Если в коде есть switch по трём своим структурам с разной логикой в каждой ветке, то это поведение просится в интерфейс: три типа, один метод, никаких веток.
Отличить просто по вопросу: добавление нового типа заставит меня править этот switch? Если да и таких мест несколько - каждый новый тип будет приносить забытую ветку. Метод в интерфейсе такой ошибки не допустит: компилятор не даст типу существовать без реализации.
// Поэтому в обзоре кода на переключатель по типу смотрят с подозрением. Он не запрещён, но требует ответа на вопрос, почему здесь не метод.
Как отвечать: «Какого типа переменная в ветке type switch и что ловит case nil?»
В каждой ветке переменная имеет тип именно этой ветки: в ветке с конкретной структурой это структура со всеми полями, в ветке со строкой - строка. Работать с ней можно сразу, дополнительного утверждения не нужно, и в этом главное удобство конструкции по сравнению с цепочкой if. Исключение одно: если в ветке перечислено несколько типов через запятую, переменная остаётся интерфейсной - компилятору просто не из чего выбрать. То же самое в default. case nil срабатывает, когда интерфейс пуст, то есть оба его внутренних слова нулевые. Важно не перепутать: интерфейс, внутри которого лежит nil-указатель конкретного типа, туда не попадёт - он уйдёт в ветку своего типа, потому что тип в паре записан. Это та же граница, что и в ловушке typed nil. И отдельно: для разбора ошибок переключатель по типу не подходит, потому что не разворачивает цепочку обёрток - там нужен errors.As. А вообще разбор по типам собственных доменных типов я стараюсь заменять методом в интерфейсе: иначе каждый новый тип будет приносить забытую ветку.
Правило про тип переменной названо с обоими исключениями, граница case nil увязана с устройством интерфейса, и добавлено суждение о том, когда конструкция вообще уместна.
На чём валят
- −Ждут конкретный тип переменной в ветке с несколькими типами через запятую.
- −Думают, что case nil поймает интерфейс с nil-указателем внутри.
- −Ставят широкий интерфейс выше узкого, и узкая ветка не срабатывает никогда.
- −Разбирают обёрнутые ошибки переключателем по типу вместо errors.As.
- −Держат switch по своим доменным типам там, где напрашивается метод.
- −Забывают default и молча пропускают неизвестный тип.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 4, остальные разбираются в тренажёре.
- Что делает конструкция switch v := x.(type) { case int: … case string: … }?A)Сравнивает значение x с константами int и stringB)Ветвится по динамическому типу x, связывая v с типом веткиC)Преобразует x в каждый тип по очереди, пока не выйдет без паникиD)Работает только с типами-числами; для строк нужен обычный switch
показать ответ и разбор
+B)Ветвится по динамическому типу x, связывая v с типом ветки// разбор: type switch ветвится по ДИНАМИЧЕСКОМУ типу интерфейсного значения. В каждой ветке v уже нужного типа (в case int — v это int). Это читаемая альтернатива цепочке ассерций, когда обрабатывают несколько возможных типов. default ловит остальные. Одна ветка может перечислять несколько типов через запятую (тогда v остаётся интерфейсом).
- В type switch одна ветка: case int, int64:. Какого типа v в этой ветке?A)int — компилятор берёт первый перечисленный типB)int64 — берётся более широкий из перечисленных типовC)Зависит от значения — v получит тот тип, что реально лежит внутриD)Остаётся интерфейсом x — при нескольких типах ветки v не сужается
показать ответ и разбор
+D)Остаётся интерфейсом x — при нескольких типах ветки v не сужается// разбор: Если в одной ветке type switch перечислено НЕСКОЛЬКО типов (case int, int64:), компилятор не может дать v один конкретный тип — поэтому v остаётся исходного интерфейсного типа (как x). Сузить до конкретного можно лишь в ветке с единственным типом. Частая ошибка — ждать в такой ветке готовый int и получить ошибку компиляции.
- Что означает ветка case nil в конструкции switch v := x.(type)?A)Что интерфейс содержит указатель, но он ещё не инициализированB)Что сам интерфейс x не содержит ни типа, ни значенияC)Что тип в интерфейсе не совпал ни с одной из перечисленных ветокD)Что динамическое значение внутри интерфейса равно nil
показать ответ и разбор
+B)Что сам интерфейс x не содержит ни типа, ни значения// разбор: case nil ловит именно пустой интерфейс — когда внутри нет ни типа, ни значения. Это не то же самое, что интерфейс с типом *T и нулевым указателем внутри: такой попадёт в ветку case *T, потому что тип у него есть. Ровно на этом различии и построена ловушка typed nil, из-за которой err != nil оказывается истиной при «нулевой» ошибке.
- Значение подходит сразу под два case в type switch (конкретный тип и интерфейс, который он реализует). Какая ветка сработает?A)Обе по очереди, как в switch без условия с fallthroughB)Ветка с конкретным типом — она приоритетнее интерфейснойC)Первая подходящая сверху вниз по порядку в кодеD)Компилятор сообщит о неоднозначности и потребует разделить ветки
показать ответ и разбор
+C)Первая подходящая сверху вниз по порядку в коде// разбор: Ветки проверяются по порядку, и выполняется первая совпавшая; fallthrough в type switch запрещён. Практическое следствие: конкретные типы ставят выше интерфейсных, иначе широкая ветка вроде case error перехватит всё и специализированная обработка ниже никогда не выполнится. Не совпало ничего — сработает default.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.