x.(T): достаём значение из интерфейса
Значение интерфейса скрывает конкретный тип, который в нём лежит. Утверждение типа x.(T) просит вернуть его.
Вывод:
gopher 6
0 false
string of length 6
x должен иметь интерфейсный тип. Утверждение на конкретном типе не компилируется: invalid operation: s (variable of type string) is not an interface.
Форма с одним значением паникует
Если утверждение неверно, форма с одним значением паникует с сообщением, где названы оба типа:
Вывод:
recovered: interface conversion: interface {} is int, not string
Используйте форму с одним значением, только когда другой тип означает ошибку программиста, на которой лучше упасть. Во всех остальных случаях используйте comma-ok. При неудаче comma-ok даёт нулевое значение T и false.
Утверждение на nil-интерфейсе тоже не проходит: var v any; v.(string) паникует с interface conversion: interface {} is nil, not string, а форма comma-ok возвращает "", false.
Утверждение к другому интерфейсу
T может быть интерфейсным типом. Тогда утверждение успешно, если динамическое значение реализует T, и результат хранит то же самое динамическое значение. Так стандартная библиотека проверяет необязательные возможности.
io.Copy проверяет, реализует ли источник io.WriterTo, и если да, использует его. fmt проверяет Stringer и error. Этот шаблон позволяет маленькому интерфейсу оставаться маленьким, а вызывающему коду всё равно пользоваться более богатыми реализациями.
Type switch
Когда значение может быть одного из нескольких типов, type switch заменяет цепочку утверждений. x.(type) допустимо только внутри switch.
Вывод:
hi
integer 7
integer 8
3.14
nil
error: boom
unhandled []int
Что полезно знать:
- В ветке с одним типом
xимеет этот тип. В ветке с несколькими типами и вdefaultxимеет тип исходного интерфейса (здесьany). - Ветки проверяются по порядку. Ставьте более конкретные интерфейсы раньше общих, ведь значение может удовлетворять нескольким.
case nilсовпадает только с nil-интерфейсом, а не с интерфейсом, в котором лежит nil-указатель.fallthroughв type switch нет.
errors.As: утверждение для обёрнутых ошибок
Ошибки часто оборачивают контекстом: fmt.Errorf("load config: %w", err). Прямое утверждение типа смотрит только на самую внешнюю ошибку и пропускает ту, что внутри. errors.As проходит по цепочке.
Вывод:
type assertion finds it: false
errors.As finds it: open /no/such/file
Применяйте утверждение типа к ошибкам, только если точно знаете, что ошибка не обёрнута, а на практике это почти никогда. errors.As, errors.Is и определение собственных типов ошибок разобраны на странице о собственных ошибках.
Утверждения, преобразования и дженерики
| У вас есть | Нужно | Что использовать |
|---|---|---|
int | float64 | преобразование: float64(n) |
any, в котором лежит int | сам int | утверждение: v.(int) |
any одного из нескольких типов | ветвление по типу | type switch |
error, который может быть обёрнут | конкретный тип ошибки | errors.As |
| функция для многих типов | безопасность на этапе компиляции | дженерики |
Код, полный any и type switch, часто говорит о том, что дженерики или нормальный интерфейс лучше выразили бы намерение и ловили бы ошибки на этапе компиляции.
Частые ошибки
- Паникующая форма на непроверенных данных. Декодированный JSON, значения мапы типа
any, входные данные плагинов: всегда comma-ok. - Утверждение к не тому числовому типу. Числа JSON декодируются в
anyкакfloat64, поэтомуv.(int)на них не срабатывает. - Утверждение к типу-значению, когда хранится указатель. Если в интерфейсе лежит
*User, тоv.(User)не пройдёт. Пишитеv.(*User). - Утверждения типа на ошибках. Используйте
errors.As.
Часто задаваемые вопросы
Что такое утверждение типа в Go?
Выражение x.(T), где x это значение интерфейса. Если T конкретный тип, оно возвращает хранящееся в x значение как T. Если T интерфейсный тип, оно проверяет, что хранящееся значение тоже реализует T. Форма с одним значением паникует, если проверка не прошла; форма с двумя значениями v, ok := x.(T) сообщает об этом через ok.
Как проверить тип значения интерфейса в Go?
Используйте type switch: switch v := x.(type) { case int: ...; case string: ...; default: ... }. Внутри каждой ветки v имеет тип этой ветки. Для одного типа короче утверждение comma-ok s, ok := x.(string). fmt.Printf("%T", x) печатает имя типа для отладки.
Чем утверждение типа отличается от преобразования типа в Go?
Преобразование T(x) превращает значение одного типа в другой, например float64(n); допустимо ли оно, решает компилятор, и во время выполнения оно не может завершиться неудачей. Утверждение x.(T) работает только со значениями интерфейсов и во время выполнения проверяет, какой тип хранится внутри. Для any, в котором лежит int, нельзя написать int(x); нужно x.(int).
Стоит ли проверять типы ошибок через утверждение типа?
Нет. Используйте errors.As(err, &target). Прямое утверждение err.(*MyError) смотрит только на внешнюю ошибку, поэтому перестаёт срабатывать, как только ошибку обернули через fmt.Errorf("...: %w", err). errors.As проходит по всей цепочке обёрток.