Type assertion и type switch
Type assertion: достаём значение из интерфейса
Когда переменная имеет интерфейсный тип, компилятор знает о ней только методы интерфейса — но не конкретный тип. Type assertion — это способ сказать компилятору: «я знаю, что здесь лежит вот этот конкретный тип».
var i interface{} = "hello"
// Двухзначная форма — безопасная
s, ok := i.(string)
fmt.Println(s, ok) // hello true
n, ok := i.(int)
fmt.Println(n, ok) // 0 false (нулевое значение, ok=false)Двухзначная форма никогда не паникует: если тип не совпадает, ok будет false, а переменная получит нулевое значение.
Опасная форма: без ok
Есть однозначная форма без ok:
s := i.(string) // ok, если i содержит string
n := i.(int) // PANIC: interface conversion: interface {} is string, not intИспользуйте её только когда абсолютно уверены в типе. На практике это редкость — предпочитайте двухзначную форму.
// Типичный паттерн
func getString(v any) (string, bool) {
s, ok := v.(string)
return s, ok
}Type assertion с интерфейсом как целевым типом
Assertion работает не только на конкретные типы — можно проверить, реализует ли значение другой интерфейс:
type Stringer interface {
String() string
}
type Writer interface {
Write([]byte) (int, error)
}
func tryStringer(v any) {
if s, ok := v.(Stringer); ok {
fmt.Println("реализует Stringer:", s.String())
} else {
fmt.Println("не реализует Stringer")
}
}Это мощный паттерн: в fmt так и устроен вывод — fmt.Println проверяет, есть ли у значения метод String() string.
Type switch: несколько типов сразу
Когда нужно обработать много вариантов типов, цепочка if ok := v.(T) становится громоздкой. Type switch решает это элегантно:
func describe(i any) {
switch v := i.(type) {
case int:
fmt.Printf("int: %d (удвоенное: %d)\n", v, v*2)
case string:
fmt.Printf("string: %q (длина: %d)\n", v, len(v))
case bool:
fmt.Printf("bool: %v\n", v)
case []int:
fmt.Printf("[]int: len=%d\n", len(v))
case nil:
fmt.Println("nil")
default:
fmt.Printf("неизвестный тип: %T\n", v)
}
}Важные особенности type switch:
v := i.(type)— специальный синтаксис, работает только вswitch- В каждом
caseпеременнаяvуже имеет конкретный тип ветки - Несколько типов в одном
case:case int, int64:
Реальные паттерны
Обработка ошибок с дополнительным контекстом:
type NotFoundError struct {
Resource string
}
func (e *NotFoundError) Error() string {
return e.Resource + " not found"
}
func handleError(err error) {
if nfe, ok := err.(*NotFoundError); ok {
log.Printf("ресурс не найден: %s", nfe.Resource)
return
}
log.Printf("общая ошибка: %v", err)
}Опциональные интерфейсы (optional interface):
type Closer interface {
Close() error
}
func closeIfPossible(v any) {
if c, ok := v.(Closer); ok {
c.Close()
}
}Так работает http.ResponseWriter — сервер проверяет, поддерживает ли конкретный ResponseWriter интерфейс http.Flusher или http.Hijacker.
Compile-time проверка интерфейса
Хотите убедиться, что тип реализует интерфейс прямо во время компиляции? Используйте идиому с _:
type MyWriter struct{}
func (w *MyWriter) Write(p []byte) (int, error) {
return len(p), nil
}
// Compile-time assertion: *MyWriter должен реализовывать io.Writer
var _ io.Writer = (*MyWriter)(nil)Если *MyWriter не реализует io.Writer — получим ошибку компилятора, а не панику в рантайме. Размещайте такие проверки рядом с объявлением типа — они служат документацией.
Итого: когда что использовать
| Задача | Инструмент |
|---|---|
| Достать один конкретный тип | v, ok := i.(Type) |
| Обработать несколько типов | switch v := i.(type) |
| Проверить реализацию интерфейса | v, ok := i.(Interface) |
| Compile-time гарантия | var _ I = (*T)(nil) |
Type assertion — хирургический инструмент. Если вы часто делаете assertion на один и тот же тип, подумайте: может, лучше сделать функцию принимающей конкретный тип или специализированный интерфейс?