Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Как интерфейс устроен внутри?
Интерфейсное значение — пара из двух слов: указатель на таблицу методов (itab: тип + набор функций) и указатель на данные. Отсюда следствия, которые любят проверять: интерфейс «знает» свой динамический тип, присваивание в интерфейс может копировать значение (или упаковывать указатель), а сравнение двух интерфейсных значений сравнивает и тип, и данные. Размер любого интерфейсного значения фиксирован — два машинных слова, независимо от того, что под ним лежит.
2. Почему вызов метода на nil-интерфейсе паникует, а на nil-указателе конкретного типа — нет?
Два разных nil. Вызов метода на nil-интерфейсе паникует: под интерфейсом нет ни типа, ни данных, рантайму не на что смотреть в таблице методов. Вызов метода на nil-указателе конкретного типа (*T) — законен, если метод не разыменовывает приёмник: приёмник равен nil, но метод получает его как аргумент и может проверить. Ловушка посерьёзнее: интерфейс, в который положили типизированный nil-указатель, НЕ равен nil — внутри есть тип! Это классика: функция возвращает (*T)(nil) как error, вызывающий проверяет err != nil и получает true при отсутствии ошибки. Правило: возвращайте error как error — nil только явно.
var r io.Reader // nil-интерфейс r.Read(buf) // паника: нет типа внутри var f *os.File = nil var r2 io.Reader = f // интерфейс НЕ nil: внутри тип *os.File fmt.Println(r2 != nil) // true
3. Чем type assertion отличается от type switch?
type assertion v := x.(T) извлекает значение конкретного типа: без ok при неверном типе — паника, с comma-ok (v, ok := x.(T)) — просто false. type switch перебирает типы: switch v := x.(type) { case int: … case string: … } — в каждой ветке v уже нужного типа; case nil ловит интерфейс без значения; вариант с несколькими типами в одном case отдаёт v как исходный интерфейс. Правило: одно-два типа — assertion с ok; ветвление по многим типам — type switch; если поведение одинаково для группы типов, лучше объявить их через интерфейс-ограничение, а не свитч.
switch v := x.(type) {
case int:
fmt.Println("число", v+1)
case string:
fmt.Println("строка", len(v))
case nil:
fmt.Println("пусто")
default:
fmt.Println("неизвестно")
}4. Где объявлять интерфейс — рядом с типом или рядом с потребителем?
Идиома Go: интерфейс объявляет потребитель, а не поставщик. io.Reader живёт в io, потому что его используют все функции чтения, а не потому, что так задумали авторы файлов. Следствие для дизайна: не создавайте «интерфейс на каждый тип заранее» (привычка из Java) — абстракция без второго воплощения это спекуляция. Мелкий интерфейс из одного-двух методов (Reader, Stringer) легче реализовать, тестировать и подменить. На собеседовании это вопрос про вкус и понимание слабых связей.
5. Чем дженерики отличаются от интерфейсов и когда что использовать?
Дженерики (type parameters, Go 1.18+) решают задачу «одинаковый код для разных типов» на этапе компиляции: без боксации, с проверкой типов на компиляции, с ограничениями через constraint-интерфейсы (comparable, кастомные ограничения с методами и union-типами). Интерфейсы решают задачу «разное поведение за одной границей» — динамическая диспетчеризация в рантайме. Правило: одинаковая логика, разные типы (контейнеры, сортировки) — дженерики; разные реализации одного контракта, подмена в тестах, плагины — интерфейсы. Антипаттерн — дженерики ради дженериков: если алгоритм одинаков для всех T без ограничений, скорее всего вам нужен просто интерфейс.
func Max[T cmp.Ordered](a, b T) T { // одинаковый код, разные типы
if a > b { return a }
return b
}
var w io.Writer = buf // разное поведение за одной границей6. Что такое embedding интерфейсов и где он полезен?
Интерфейс может встраивать другие интерфейсы: io.ReadWriteCloser — это Reader + Writer + Closer в одном наборе методов. Так собирают «большие» контракты из мелких без дублирования сигнатур. Полезен для грациозного деградирования функции: функция принимает узкий интерфейс, а документирует через встроенный широкий. Ловушка на собеседовании: embedding — это НЕ наследование реализации, только объединение наборов методов; «переопределить» унаследованный метод нельзя — если тип определяет метод с той же сигнатурой, он просто его реализует сам.
7. Как проверить, что тип реализует интерфейс, на этапе компиляции?
Идиома с нулевым значением: var _ io.Reader = (*MyType)(nil) — компилятор проверит набор методов, не создавая экземпляр и не тратя рантайм. Без этой проверки отсутствие метода всплывёт только в месте присваивания, возможно в другом файле. В стандартной библиотеке идиома стоит почти перед каждой реализацией интерфейса — стоит взять в привычку в своих пакетах.
var _ io.Reader = (*MyFile)(nil) // компилятор проверит реализацию
8. Почему empty interface (any) считается код-смеллом? Когда он всё же нужен?
any стирает типы: внутри функции значение надо приводить обратно через assertion, компилятор ничего не проверяет, ошибки переезжают в рантайм. С Go 1.18 большую часть «any-хвостов» закрывают дженерики: контейнер T вместо any, функция с ограничением вместо «принимаю что угодно». Когда any оправдан: границы с нетипизированными данными (encoding/json.Unmarshal в map[string]any), печать произвольных значений (fmt), кэши произвольных объектов до введения типов. Формулировка для интервьюера: «any — это подпись „типы заканчиваются здесь, дальше ручная проверка“ — и она должна быть осознанной».
9. Что произойдёт при сравнении двух интерфейсных значений, под которыми лежат несовместимые типы?
Паника только в одном случае: если динамические типы совпадают и этот тип несравним (срез, карта, функция) — сравнение паникует «comparing uncomparable type». Если типы разные — результат false без паники. Отсюда тонкая бага: err из функции с типом *MyErr внутри интерфейса нельзя надёжно сравнивать через == — используйте errors.Is/As, которые работают с обёртками и не зависят от сравнимости.
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Язык Go с нуля: базовый синтаксис и конструкции — Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.