Интерфейсы, nil и type assertion в Go
Зачем это на интервью
Интерфейсы в Go задают поведение структурно: тип реализует их без ключевого слова implements. Это делает API небольшими и гибкими, но требует точно понимать dynamic type, dynamic value и nil. Классическая production-ошибка — вернуть (*MyError)(nil) как error, получить non-nil interface и выполнить ненужную ветку ошибки.
Минимум для E4
- Объяснить, что interface value содержит dynamic type и dynamic value.
- Отличать nil interface от interface, содержащего typed-nil pointer.
- Объявлять узкий интерфейс у потребителя, а не экспортировать «универсальный» интерфейс поставщика.
- Использовать comma-ok assertion или type switch для данных из открытой границы.
Nil interface не содержит ни dynamic type, ни dynamic value. После присваивания nil-указателя интерфейсу dynamic type уже известен, поэтому интерфейс не равен nil.
package main
import "fmt"
type FileStore struct{}
func (*FileStore) String() string { return "file store" }
type Worker interface {
Work() string
}
type job struct{}
func (*job) Work() string { return "done" }
func main() {
var p *job
var w Worker = p
fmt.Println(p == nil) // true
fmt.Println(w == nil) // false: dynamic type is *job
}Присваивание интерфейсу проверяет method set на этапе компиляции. Хорошая защита для exported реализации — явная проверка:
package main
import "io"
type FileStore struct{}
func (*FileStore) Read(p []byte) (int, error) { return 0, io.EOF }
var _ io.Reader = (*FileStore)(nil)
func main() {}Углубление для E5/Senior
Интерфейсная граница должна выражать потребность вызывающего кода. Интерфейс из одного-двух методов легче реализовать, документировать и эволюционировать. Принимайте интерфейсы, возвращайте concrete types, если API не обязан скрывать реализацию: это сохраняет методы и снижает необходимость assertions.
Не все значения можно безопасно сравнивать через ==. Два interface values сравниваются только если их dynamic values comparable; интерфейс с []byte, map или func при сравнении может вызвать panic. Для данных use slices.Equal, maps.Equal, reflect.DeepEqual с пониманием его семантики либо доменное сравнение.
Type assertion v.(T) паникует, если dynamic type не соответствует T. Двухзначная форма value, ok := v.(T) не паникует. В type switch case с nil срабатывает только для nil interface; typed nil попадёт в case его pointer-типа. После проверки pointer всё ещё может быть nil — это нужно обработать до вызова метода, если метод не поддерживает nil receiver.
package main
import "fmt"
func describe(v any) string {
switch x := v.(type) {
case nil:
return "nil interface"
case string:
return "string: " + x
case *FileStore:
if x == nil {
return "typed-nil *FileStore"
}
return x.String()
case fmt.Stringer:
// x — non-nil interface even when it contains a typed-nil pointer.
return x.String()
default:
return fmt.Sprintf("unknown %T", x)
}
}
func main() {
fmt.Println(describe("go"))
}any — alias для interface{}; он не устраняет динамическую типизацию. Если набор типов известен на compile time, generics или обычный concrete API обычно лучше, чем any с assertions. Для ошибок используйте errors.As, а не assertion: wrapping меняет внешний dynamic type, но не логическую классификацию ошибки.
Ключевые понятия
| Понятие | Смысл | Последствие |
|---|---|---|
| Interface value | Пара dynamic type + dynamic value | nil только когда отсутствуют оба |
| Typed nil | Nil pointer/map/slice и известный dynamic type | Interface с ним обычно != nil |
| Implicit implementation | Совпадение method set без декларации | Контракт можно определить возле потребителя |
| Type assertion | Проверка/извлечение dynamic type | Однозначная форма может panic |
| Type switch | Ветвление по dynamic type | Удобен для закрытого набора допустимых типов |
Пустой интерфейс принимает любое значение, но не предоставляет полезных операций без assertion. Не используйте его вместо предметной модели или чтобы обойти циклические зависимости: обычно это переносит ошибку из компиляции в runtime.
Типовые вопросы
- Почему
return (*MyError)(nil)какerrorопасен?- Интерфейс содержит тип
*MyError, поэтому не равенnil; возвращайте literalnilпри отсутствии ошибки.
- Интерфейс содержит тип
- Чем
var r io.Readerотличается отvar r io.Reader = (*bytes.Buffer)(nil)?- Первый — nil interface; второй содержит dynamic type
*bytes.Bufferи typed-nil dynamic value.
- Первый — nil interface; второй содержит dynamic type
- Когда применять
x.(T)безok?- Только когда нарушение инварианта — программная ошибка и panic действительно допустима; на входе API используйте comma-ok.
- Где объявлять интерфейс?
- Обычно рядом с потребителем, с минимальным набором нужных ему методов.
- Почему
anyне является generic?anyхранит runtime dynamic type; generic type parameter проверяется и специализируется компилятором в рамках constraint.
- Почему не стоит возвращать интерфейс всегда?
- Клиент теряет дополнительные методы concrete type и вынужден делать assertions; возвращайте interface только как намеренную абстракцию.
Практика
- Воспроизведите typed-nil ошибку в функции
func Open() errorи исправьте контракт.- Критерии готовности: тест различает
nilи typed nil; успешная ветка возвращает literalnil;go vetиgo testпроходят.
- Критерии готовности: тест различает
- Спроектируйте порт
Notifierдля use case с методами, нужными только use case.- Критерии готовности: интерфейс содержит не более двух методов; fake реализует его без SDK; compile-time assertion подтверждает production adapter.
- Распарсите
[]anyсstring,float64и неподдерживаемым значением.- Критерии готовности: нет panic на внешнем вводе; неизвестный тип даёт диагностическую ошибку с
%T; typed nil обработан отдельно.
- Критерии готовности: нет panic на внешнем вводе; неизвестный тип даёт диагностическую ошибку с
Частые ошибки и ловушки
- Проверять лишь
err != nil, когда функция формирует typed-nil error. - Вызывать метод у typed-nil pointer внутри interface без контракта на nil receiver.
- Делать assertion на конкретную ошибку вместо
errors.As. - Добавлять широкий интерфейс из десятков методов ради «переиспользования».
- Сравнивать interface values, внутри которых могут лежать slice, map или func.