Interfaces Internals — внутренности интерфейсов Go
Что это: углублённый разбор того, как интерфейсы устроены в runtime Go — структуры
iface/eface,itab, кеш, dispatch. Зачем знать на Middle 1: на собеседовании обязательно спросят про nil-interface trap, про cost dynamic dispatch и про то, что внутри пары “type + value”. Без понимания исходниковruntime/iface.goответ будет поверхностным.
Содержание (TOC)
Заголовок раздела «Содержание (TOC)»- Базовая концепция (краткое повторение)
- Под капотом: iface, eface, itab — как это устроено в runtime
- Тонкие моменты / Gotchas (12+)
- Производительность и compiler optimizations
- Когда использовать / когда НЕ использовать
- Вопросы на собесе Middle 1 (25+)
- Practice — задачи (7)
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»Интерфейс в Go — это множество методов, которое тип удовлетворяет неявно (structural typing, duck typing). Никакого implements ключевого слова нет: если у типа есть все методы интерфейса с правильной сигнатурой — он автоматически удовлетворяет интерфейс.
type Reader interface { Read(p []byte) (n int, err error)}
type File struct { /* ... */ }func (f *File) Read(p []byte) (int, error) { /* ... */ }
var r Reader = &File{} // *File автоматически удовлетворяет ReaderОсобенности:
any(с Go 1.18) — алиасinterface{}.- Интерфейс может быть пустым (
any/interface{}) — удовлетворяется любым типом. - Интерфейс — значение из двух слов на 64-bit платформе (16 байт).
- Метод можно вызывать через интерфейс — это dynamic dispatch (виртуальный вызов).
Этого хватит для junior-уровня. Дальше идёт middle 1 — что именно лежит внутри этих 16 байт.
2. Под капотом
Заголовок раздела «2. Под капотом»2.1 Две структуры: iface и eface
Заголовок раздела «2.1 Две структуры: iface и eface»В исходниках runtime/runtime2.go (Go 1.22+):
// iface — интерфейс с методами (нон-emptу интерфейс)type iface struct { tab *itab data unsafe.Pointer}
// eface — пустой интерфейс (interface{} / any)type eface struct { _type *_type data unsafe.Pointer}Различие принципиальное:
ifaceхранит указатель на itab (interface table) — там и тип, и таблица методов.efaceхранит указатель только на _type (тип) — методов нет, их и не нужно.
iface (16 байт): eface (16 байт):+--------+--------+ +--------+--------+| *itab | *data | | *_type | *data |+--------+--------+ +--------+--------+ 8B 8B 8B 8BПоэтому когда говорят “интерфейс занимает 2 слова” — это про оба варианта. unsafe.Sizeof(any(nil)) вернёт 16 на 64-битной платформе.
2.2 Структура itab
Заголовок раздела «2.2 Структура itab»itab — самое интересное. Из runtime/runtime2.go:
type itab struct { inter *interfacetype // тип интерфейса (какой именно — Reader, io.Writer и т.д.) _type *_type // конкретный тип (например, *os.File) hash uint32 // копия _type.hash для быстрой проверки в type switch _ [4]byte fun [1]uintptr // ← массив указателей на методы (на самом деле variadic)}Поле fun объявлено как [1]uintptr, но фактически это variable-length array — после хедера лежат N указателей на функции, где N — число методов в интерфейсе. Runtime обращается к fun[0], fun[1], … через арифметику указателей.
itab struct (для интерфейса io.ReadWriter, конкретный тип *os.File):+----------------+--------------------------------+| inter | -> &interfacetype{io.ReadWriter}|| _type | -> &_type{*os.File} || hash | 0xDEADBEEF || pad | || fun[0] | -> (*os.File).Read || fun[1] | -> (*os.File).Write |+----------------+--------------------------------+Когда мы пишем r.Read(buf), где r — это io.Reader (на самом деле *os.File):
- Компилятор знает, что
Read— это первый метод интерфейса (индекс 0). - Генерируется ассемблер: загрузить
r.tab.fun[0], вызвать его с аргументами(r.data, buf). - Метод получает свой ресивер через
r.data(указатель наos.File).
2.3 itab cache
Заголовок раздела «2.3 itab cache»itab для пары (interface_type, concrete_type) создаётся один раз и кешируется. В runtime/iface.go:
// Реальный фрагмент (упрощённо):var itabTable atomic.Pointer[itabTableType]
type itabTableType struct { size uintptr count uintptr entries [1]*itab // hash table}
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab { // 1. Хеш-поиск в itabTable // 2. Если не найдено — итерация по методам, проверка satisfy // 3. Если satisfy — создание нового itab, добавление в таблицу}Ключевые моменты:
- Хеш-таблица глобальная, lock-free для чтения.
- При записи (создание нового itab) — берётся
itabLock. - Размер таблицы растёт при достижении лимита загрузки.
- Поэтому первая конверсия
*os.File → io.Readerдороже, последующие — почти бесплатные.
Lookup путь: iface(r, *os.File) → hash(io.Reader, *os.File) → itabTable[h] → *itab → fun[0] ↑ ↑ O(1) хеш O(1) lookup2.4 Конверсия типа → интерфейс
Заголовок раздела «2.4 Конверсия типа → интерфейс»Когда пишем:
var r io.Reader = file // file типа *os.FileКомпилятор вставляет вызов:
runtime.convT2I(itab_*os.File_io.Reader, &file)или (для не-указательных типов) convT2I копирует значение в heap и возвращает указатель.
Для eface (пустой интерфейс):
var x any = 42Компилятор вставляет runtime.convT2E(_type_int, &42).
⚠️ Важно: запись скаляра (int, bool, float64) в any аллоцирует на heap:
var x any = 42 // ← allocation! 42 копируется в heap.var x any = "hello" // ← без allocation для строк (они уже два слова на heap-данные) // но wrapper создаётся (eface)В Go 1.15+ есть оптимизация — для small integers (0-255) аллокации может не быть, используется кеш. Но для других значений — будет.
2.5 Type assertion: FAST path vs SLOW path
Заголовок раздела «2.5 Type assertion: FAST path vs SLOW path»v, ok := r.(*os.File) // type assertion с проверкойv := r.(*os.File) // type assertion с panic при несовпаденииВ runtime есть несколько функций:
| Функция | Когда вызывается |
|---|---|
assertI2I | interface → interface (с методами) |
assertE2I | empty interface → interface (с методами) |
assertI2T | interface → concrete type |
assertE2T | empty interface → concrete type |
typeAssert | Generic версия (Go 1.18+) |
FAST path для assertE2T (eface → concrete):
if eface._type == target_type { return eface.data, true}return zero, falseSLOW path для assertI2I (iface → другой интерфейс):
if iface.tab.inter == target_interface { return iface, true // тот же интерфейс}// Нужно создать новый itab — вызвать getitabnewtab := getitab(target_interface, iface.tab._type, true)if newtab == nil { return zero, false}return iface{tab: newtab, data: iface.data}, trueПоэтому конверсия между разными интерфейсами дороже, чем между concrete type и интерфейсом.
2.6 Type switch internals
Заголовок раздела «2.6 Type switch internals»switch v := r.(type) {case *os.File: // ...case *bytes.Buffer: // ...default:}Компилятор генерирует что-то вроде:
hash := r.tab._type.hash // или r._type.hash для efaceswitch hash {case hash_of_os_File: if r.tab._type == os_File_type { goto case1 }case hash_of_bytes_Buffer: if r.tab._type == bytes_Buffer_type { goto case2 }}goto defaultТо есть — это switch по хешу типа, с проверкой указателя для disambiguation (хеши могут совпадать). Сложность O(1) при использовании jump table или O(log N) при бинарном поиске.
2.7 Inlining метода через интерфейс
Заголовок раздела «2.7 Inlining метода через интерфейс»Это редко возможно. Компилятор обычно не может инлайнить вызов через интерфейс, потому что не знает конкретный тип в compile-time.
Исключение — devirtualization (Go 1.20+ улучшил это):
var r io.Reader = &bytes.Buffer{...}n, _ := r.Read(buf) // компилятор может догадаться: r — это *bytes.Buffer → инлайнDevirtualization работает только в простых случаях, когда escape analysis видит, что значение интерфейса не покидает функцию.
2.8 Dynamic dispatch cost
Заголовок раздела «2.8 Dynamic dispatch cost»Замер на современном x86_64 (Go 1.22, без MFENCE):
| Тип вызова | ns/op | Inline? |
|---|---|---|
| Direct call | ~0.3 ns | yes |
| Interface call | ~1.5 ns | rarely |
| Reflect call | ~150 ns | no |
Overhead интерфейса — это:
- Загрузка
iface.tab.fun[i](одна indirection). - Indirect call (branch predictor может ошибиться).
- Блокировка inlining → нельзя оптимизировать аргументы, escape, и т.д.
На горячем пути (10^9 вызовов) разница может быть существенной. На холодном — не имеет значения.
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»Gotcha 1: nil interface — он не всегда то, что вы думаете
Заголовок раздела «Gotcha 1: nil interface — он не всегда то, что вы думаете»func returnError() error { var err *MyError = nil return err // err — non-nil interface, hodling nil pointer!}
func main() { err := returnError() if err != nil { fmt.Println("ERROR:", err) // ← сюда попадём! }}Почему: error — это интерфейс. Когда мы возвращаем err (типа *MyError), runtime пакует его в iface{tab: itab_MyError_error, data: nil}. Поле tab — non-nil, поэтому err != nil — true. Но data — nil.
⚠️ Подвох: проверка if err != nil после возврата конкретного указателя через интерфейс — классический баг. Всегда возвращайте nil (без типа) или явно проверяйте на nil перед возвратом.
func returnError() error { var err *MyError = nil if err != nil { return err } return nil // ← правильно}Gotcha 2: nil pointer внутри non-nil interface — panic при вызове метода
Заголовок раздела «Gotcha 2: nil pointer внутри non-nil interface — panic при вызове метода»var r io.Reader = (*MyReader)(nil)r.Read(buf) // ← panic: invalid memory address or nil pointer dereferenceИнтерфейс non-nil (есть *itab), но data == nil. Когда вызывается метод — он передаст nil как ресивер. Если метод обращается к полям — panic. Если не обращается (например, func (m *MyReader) Hello() {} без обращений к m) — работает.
Gotcha 3: интерфейсы и compile-time проверка satisfy
Заголовок раздела «Gotcha 3: интерфейсы и compile-time проверка satisfy»Часто пишут:
var _ io.Reader = (*MyReader)(nil)Эта строка ничего не делает в runtime, но в compile-time проверяет, что *MyReader удовлетворяет io.Reader. Если нет — ошибка компиляции.
Полезно для:
- Документации (явно показано, что тип реализует интерфейс).
- Раннего обнаружения breaking changes (если интерфейс изменился).
Gotcha 4: указатель vs значение в method set
Заголовок раздела «Gotcha 4: указатель vs значение в method set»type Animal interface { Sound() string}
type Dog struct{}func (d *Dog) Sound() string { return "woof" } // ← pointer receiver
var a Animal = Dog{} // ← compile error: Dog doesn't implement Animalvar a Animal = &Dog{} // ← окПравило: method set типа T содержит только методы с value receiver. Method set типа *T содержит и value, и pointer receiver.
Поэтому:
Dog— не удовлетворяетAnimal(нетSound).*Dog— удовлетворяет.
Это касается ТОЛЬКО автоматических преобразований через интерфейс. Прямой вызов Dog{}.Sound() работает (Go автоматически берёт адрес).
Gotcha 5: embedded interfaces — diamond problem
Заголовок раздела «Gotcha 5: embedded interfaces — diamond problem»type A interface { Foo() }type B interface { Foo() } // тот же Footype AB interface { A; B }
type T struct{}func (T) Foo() {}
var ab AB = T{} // ok? — да! Go допускает, если сигнатуры идентичны.Но если сигнатуры разные:
type A interface { Foo() int }type B interface { Foo() string }type AB interface { A; B } // ← ошибка компиляции в Go 1.14+В старом Go (до 1.14) это компилировалось, но было невозможно реализовать. Сейчас — явная ошибка.
Gotcha 6: named return через интерфейс — escape
Заголовок раздела «Gotcha 6: named return через интерфейс — escape»func newReader() (r io.Reader) { r = &MyReader{} // ← escape to heap! return}Здесь &MyReader{} обязательно убегает в heap, потому что:
- Кладётся в интерфейс.
- Возвращается из функции.
Компилятор не может заинлайнить и оставить на стеке.
Gotcha 7: сравнение интерфейсов — может паниковать
Заголовок раздела «Gotcha 7: сравнение интерфейсов — может паниковать»var a, b anya = []int{1, 2, 3}b = []int{1, 2, 3}fmt.Println(a == b) // ← panic: runtime error: comparing uncomparable type []intИнтерфейсы сравниваются как:
(tab_a == tab_b) && (data_a == data_b)— если тип в_typecomparable, идёт глубокое сравнение.- Если тип не comparable (slice, map, function) — panic.
Безопасное сравнение через type switch + reflect.DeepEqual, или вручную.
Gotcha 8: iface vs eface — лёгкая разница в перформансе
Заголовок раздела «Gotcha 8: iface vs eface — лёгкая разница в перформансе»eface — на 1 indirection меньше (нет itab), просто _type. Поэтому:
var x any = 42_, ok := x.(int) // быстрее, чемvar r io.Reader = &MyReader{}_, ok := r.(*MyReader) // чуть медленнееРазница микроскопическая (наносекунды), но в hot path может играть.
Gotcha 9: interface{} → конкретный тип через двойной приведение
Заголовок раздела «Gotcha 9: interface{} → конкретный тип через двойной приведение»var x any = "hello"s := x.(string) // okb := []byte(x.(string)) // нужно явно: any → string → []byteПрямого x.([]byte) не получится, если x хранит string. Все type assertions — строго по конкретному типу.
Gotcha 10: интерфейсы и nil чанки
Заголовок раздела «Gotcha 10: интерфейсы и nil чанки»type T struct { x int }var t *T // nil pointervar i any = t // i — non-nil interface holding nilfmt.Println(i == nil) // false!Опять nil-interface trap. Чтобы корректно проверять:
if t == nil { var i any = nil} else { var i any = t}Или использовать reflect:
func isNilValue(i any) bool { if i == nil { return true } v := reflect.ValueOf(i) return v.Kind() == reflect.Ptr && v.IsNil()}Gotcha 11: интерфейс как map key
Заголовок раздела «Gotcha 11: интерфейс как map key»m := map[any]int{}m["foo"] = 1m[42] = 2m[[]int{1,2,3}] = 3 // ← panic при вставке (uncomparable type)Map допускает interface как key, но во время вставки проверяется comparability. Если не comparable — panic.
Gotcha 12: type assertion на ошибку — слой обёрток
Заголовок раздела «Gotcha 12: type assertion на ошибку — слой обёрток»type MyErr struct{}func (m *MyErr) Error() string { return "my err" }
func foo() error { return fmt.Errorf("wrapped: %w", &MyErr{})}
func main() { err := foo() _, ok := err.(*MyErr) // false! err — это *fmt.wrapError
var myErr *MyErr if errors.As(err, &myErr) { // ← правильно, через errors.As // ok }}errors.As рекурсивно разворачивает обёртки. Простая type assertion — не разворачивает.
4. Производительность и compiler optimizations
Заголовок раздела «4. Производительность и compiler optimizations»4.1 Стоимость операций (примерно, Go 1.22)
Заголовок раздела «4.1 Стоимость операций (примерно, Go 1.22)»Operation ns/op Notes-------------------------------------------------------------------Direct method call (concrete type) 0.3 inlinedInterface method call 1.5 one indirectionType assertion (FAST path, success) 0.8 pointer compareType assertion (SLOW path, with itab) 3.0 hash lookupType assertion (failure, with ok) 0.6Type switch (3 cases) 1.5 hash + compareconvT2E (interface{}-pack with alloc) 15-30 heap allocationconvT2I (interface-pack with itab cache) 5 if cached4.2 Когда компилятор может devirtualize
Заголовок раздела «4.2 Когда компилятор может devirtualize»В Go 1.20+ улучшен анализ. Можно девиртуализировать, если:
- Интерфейс объявлен и присвоен в одном функционале (без выхода в global).
- Escape analysis не “теряет” значение.
- Конкретный тип единственный и известен в compile-time.
Пример:
func process() int { var r io.Reader = &bytes.Buffer{} // компилятор знает тип buf := make([]byte, 10) n, _ := r.Read(buf) // ← может быть девиртуализирован return n}Vs:
func process(r io.Reader) int { // ← тип неизвестен, нет девиртуализации buf := make([]byte, 10) n, _ := r.Read(buf) return n}Проверить можно через go build -gcflags=-m:
./main.go:10:6: inlining call to bytes.NewBuffer./main.go:11:8: devirtualizing r.Read to *bytes.Buffer4.3 Аллокации при пакетировании в interface{}
Заголовок раздела «4.3 Аллокации при пакетировании в interface{}»func bad(x int) any { return x // ← heap allocation!}
func good(x *int) any { return x // ← без allocation (указатель копируется)}Scalar boxing в any всегда аллоцирует (кроме малых интов). Поэтому избегайте на горячем пути.
4.4 Кеш linear lookup vs hash table
Заголовок раздела «4.4 Кеш linear lookup vs hash table»itab cache использует open addressing hash table с linear probing. При коллизиях — ищет следующий слот. Размер таблицы — степень двойки, при росте загрузки > 75% — удваивается (с миграцией).
5. Когда использовать / когда НЕ использовать
Заголовок раздела «5. Когда использовать / когда НЕ использовать»Использовать интерфейсы
Заголовок раздела «Использовать интерфейсы»- Decoupling: контракт между слоями (handler → service → repository).
- Mocking: для тестирования (мокаем интерфейс).
- Polymorphism: одна функция, разные имплементации (
io.Reader,io.Writer). - Plugin system: разные стратегии через один контракт.
НЕ использовать интерфейсы
Заголовок раздела «НЕ использовать интерфейсы»- Когда тип единственный — не “интерфейсируйте на всякий случай”.
- На горячем пути (>10^6 вызовов/сек) — direct call быстрее.
- В простых DTO/struct — не нужно.
- Когда
interface{}становится “any goes” — это анти-паттерн.
Принцип: “Accept interfaces, return structs” (Postel’s law для Go). Принимайте интерфейс (узкий), возвращайте конкретный тип (точный).
6. Вопросы на собесе Middle 1
Заголовок раздела «6. Вопросы на собесе Middle 1»Q1: Что внутри значения интерфейса в Go?
A: Структура из двух слов на 64-битной платформе: для интерфейса с методами это iface{tab *itab, data unsafe.Pointer}; для пустого интерфейса — eface{_type *_type, data unsafe.Pointer}. tab указывает на itab, содержащий тип интерфейса, конкретный тип, hash и массив указателей на методы. data — указатель на сами данные.
Q2: Чем отличается iface от eface?
A: eface — это пустой интерфейс (interface{}/any), хранит только *_type и data. iface — интерфейс с методами, хранит *itab (где и тип, и таблица методов) и data. У них одинаковый размер (16 байт), но разный layout.
Q3: Что такое itab и кто его создаёт?
A: itab — interface table. Структура содержит: тип интерфейса, конкретный тип, хеш типа и указатели на методы (fun [N]uintptr). Создаётся runtime через getitab() при первой конверсии конкретного типа в интерфейс. Хранится в глобальной hash-таблице itabTable, переиспользуется при повторных конверсиях.
Q4: Сколько байт занимает interface{} на 64-битной системе?
A: 16 байт — два слова по 8 байт: указатель на тип и указатель на данные.
Q5: Опишите, что произойдёт при вызове метода через интерфейс.
A: Компилятор знает индекс метода в интерфейсе (0, 1, 2…). При вызове:
- Получить
iface.tab.fun[i]— указатель на метод конкретного типа. - Indirect call с аргументом
iface.dataкак ресивер. - Метод выполняется, получая
dataкакself/this.
Q6: Что такое nil-interface trap?
A: Когда конкретный nil-указатель упакован в интерфейс, интерфейс становится non-nil (поскольку tab != nil, хотя data == nil). Проверка if err != nil возвращает true, даже если ошибки фактически нет. Это типичный баг: всегда возвращайте nil напрямую, а не nil-указатель через интерфейс.
Q7: Что произойдёт при вызове метода у nil-интерфейса с non-nil tab?
A: Зависит от метода. Если метод не обращается к ресиверу (например, просто возвращает константу) — работает. Если обращается (читает поля, разыменовывает) — panic: nil pointer dereference.
Q8: Как Go проверяет, удовлетворяет ли тип интерфейсу?
A: В compile-time компилятор сравнивает method set типа с методами интерфейса. Если все методы интерфейса присутствуют с правильной сигнатурой — тип удовлетворяет. В runtime эта проверка не повторяется (всё уже отрезолвлено в itab).
Q9: Разница между type assertion и type switch?
A:
- Type assertion: проверка одного типа.
v, ok := x.(T)илиv := x.(T)(с panic). - Type switch: проверка нескольких типов сразу. Внутри — switch по хешу типа, более эффективно для большого числа case.
Q10: Почему var x any = 42 может аллоцировать?
A: Скаляр (int) — 8 байт, но any хранит указатель на данные. Поэтому 42 должен быть скопирован куда-то на heap, и указатель на это место — в eface.data. Для маленьких интов (0-255) есть кеш, но в общем случае — аллокация. Это можно увидеть через go build -gcflags=-m.
Q11: Что такое devirtualization?
A: Оптимизация компилятора Go (улучшена в 1.20+): когда escape analysis может определить точный тип в интерфейсе, вызов метода превращается в direct call, что делает возможным inlining. Работает в простых случаях.
Q12: Что покажет вывод?
var p *intvar x any = pfmt.Println(x == nil)A: false. x — non-nil interface, хранящий nil pointer типа *int. Это nil-interface trap.
Q13: Как корректно проверить, что интерфейс хранит nil?
A:
func isNil(i any) bool { if i == nil { return true } v := reflect.ValueOf(i) switch v.Kind() { case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func: return v.IsNil() } return false}Q14: Может ли iface == iface паниковать?
A: Да, если конкретный тип за интерфейсом не comparable (slice, map, function). Сравнение интерфейсов — это сначала сравнение типов (быстро), потом значений. Если тип не comparable — runtime panic.
Q15: Что такое embedded interfaces?
A: Интерфейс может включать другой интерфейс через embedding:
type ReadCloser interface { io.Reader io.Closer}Method set — объединение всех методов. Если методы конфликтуют (одинаковые имена с разными сигнатурами) — ошибка компиляции (с Go 1.14+).
Q16: Зачем строка var _ MyInterface = (*MyType)(nil)?
A: Compile-time проверка, что *MyType удовлетворяет MyInterface. Не создаёт значения в runtime (объявление _). Помогает поймать ошибку, если интерфейс или тип изменился.
Q17: Сколько раз создаётся itab для пары (io.Reader, *os.File)?
A: Один раз. После первой конверсии itab кешируется в itabTable и переиспользуется. Все последующие конверсии этой пары — O(1) lookup.
Q18: Почему интерфейсы блокируют inlining?
A: Компилятор не знает, какой конкретный метод будет вызван (это решается в runtime через iface.tab.fun[i]). Без знания целевой функции — нечего инлайнить. Devirtualization частично решает эту проблему в простых случаях.
Q19: Чем отличается method set от value type и pointer type?
A:
T— методы только с value receiver.*T— методы и с value receiver, и с pointer receiver. ПоэтомуTможет не удовлетворять интерфейсу, который требует методы с pointer receiver.
Q20: Что такое convT2E и convT2I?
A: Внутренние функции runtime:
convT2E— конверсия конкретного типа в empty interface (eface).convT2I— конверсия конкретного типа в interface с методами (iface). Эти функции могут аллоцировать память на heap для значения, если оно не указатель.
Q21: Где живёт itabTable?
A: Глобальная hash-таблица в runtime, lock-free для чтения. Запись (создание нового itab) защищена itabLock. Размер растёт динамически.
Q22: Чем assertI2I отличается от assertI2T?
A:
assertI2I— конверсия из одного интерфейса в другой (нужно проверить, что конкретный тип удовлетворяет новому интерфейсу; может создать новыйitab).assertI2T— конверсия из интерфейса в конкретный тип (просто сравнить указатели типов).
Q23: Может ли nil интерфейс быть равен другому nil интерфейсу?
A: Да: var a, b any = nil, nil; a == b → true. Оба имеют _type == nil и data == nil.
Q24: Зачем any была введена как alias?
A: Для читаемости. any визуально лучше передаёт намерение “любой тип”, чем interface{}. Технически — полный alias, поведение идентично.
Q25: Что произойдёт при var x any = make(chan int); var y any = make(chan int); x == y?
A: false. Чанели — comparable по reference. Два разных make(chan int) создают разные каналы, их указатели разные. Если бы они оба ссылались на один канал — true.
Q26: Почему в коде часто пишут func F(r io.Reader) вместо func F(r *os.File)?
A: Принцип DI (Dependency Inversion). Функция принимает узкий интерфейс — её можно вызвать с любым типом, реализующим Reader. Это упрощает тестирование (мокинг) и переиспользование.
Q27: Может ли встроенный интерфейс затенять методы?
A: Да, частично. Если структура имеет embedded interface и собственный метод с таким же именем, выигрывает метод структуры (так же, как обычное embedding).
7. Practice — задачи
Заголовок раздела «7. Practice — задачи»Задача 1
Заголовок раздела «Задача 1»Что выведет?
package main
import "fmt"
type MyError struct{ Msg string }func (e *MyError) Error() string { return e.Msg }
func mightFail(shouldFail bool) error { var e *MyError if shouldFail { e = &MyError{Msg: "failed"} } return e}
func main() { err := mightFail(false) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("OK") }}Решение:
Выведет Error: <nil> (или panic при попытке err.Error() из-за nil pointer, в зависимости от метода).
Причина: mightFail(false) возвращает e, где e == nil типа *MyError. При возврате через error интерфейс — iface{tab: itab_MyError_error, data: nil}. tab != nil, поэтому err != nil — true.
Чтобы вывести “OK”, надо return nil напрямую.
Задача 2
Заголовок раздела «Задача 2»Что выведет?
package main
import "fmt"
type A interface{ M() }type B interface{ N() }
type T struct{}func (t *T) M() {}
func main() { var a A = &T{} _, ok := a.(B) fmt.Println(ok)}Решение:
false. *T имеет метод M(), но не N(). Type assertion проверяет, удовлетворяет ли конкретный тип за интерфейсом a интерфейсу B. Не удовлетворяет.
Задача 3
Заголовок раздела «Задача 3»Реализуйте функцию IsNil(i any) bool, корректно определяющую nil (с учётом nil-interface trap).
Решение:
import "reflect"
func IsNil(i any) bool { if i == nil { return true } v := reflect.ValueOf(i) switch v.Kind() { case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface: return v.IsNil() } return false}Поведение:
IsNil(nil)→ trueIsNil((*int)(nil))→ trueIsNil(42)→ falseIsNil("")→ false (пустая строка — не nil)
Задача 4
Заголовок раздела «Задача 4»Что выведет?
package main
import "fmt"
type Stringer interface { String() string}
type T struct{ V string }func (t T) String() string { return t.V }
func main() { var s Stringer = T{V: "hello"} t := s.(T) t.V = "world" fmt.Println(s.String())}Решение:
hello. При s = T{V: "hello"} значение копируется в heap (через convT2I). При s.(T) — снова копия в t. Изменение t.V не затрагивает оригинал в интерфейсе.
Если бы был *T — была бы общая ссылка.
Задача 5
Заголовок раздела «Задача 5»Напишите benchmark, показывающий разницу между:
- direct call
- interface call
any-boxing
Решение:
package bench
import "testing"
type Adder interface{ Add(int) int }type IntAdder struct{ X int }func (a *IntAdder) Add(n int) int { return a.X + n }
func BenchmarkDirect(b *testing.B) { a := &IntAdder{X: 10} sum := 0 for i := 0; i < b.N; i++ { sum = a.Add(i) } _ = sum}
func BenchmarkIface(b *testing.B) { var a Adder = &IntAdder{X: 10} sum := 0 for i := 0; i < b.N; i++ { sum = a.Add(i) } _ = sum}
func BenchmarkAnyBox(b *testing.B) { for i := 0; i < b.N; i++ { var x any = i // ← boxing! _ = x.(int) }}Запустить: go test -bench=. -benchmem. Ожидаемые результаты:
- Direct: ~0.3 ns/op, 0 allocs.
- Iface: ~1.5 ns/op, 0 allocs.
- AnyBox: ~15-30 ns/op, 1 alloc/op.
Задача 6
Заголовок раздела «Задача 6»Что выведет?
package main
import "fmt"
type I interface{ F() }type T struct{}func (T) F() {}
func main() { var i1 I = T{} var i2 I = T{} fmt.Println(i1 == i2)}Решение:
true. Оба интерфейса хранят T{} (одинаковый comparable тип, равные значения). Сравнение: _type равны, data (значения) равны → true.
Если бы T содержал []int или map — был бы panic.
Задача 7
Заголовок раздела «Задача 7»Найдите все способы сломать этот код:
type Closer interface { Close() error }
func closeAll(items []Closer) { for _, item := range items { item.Close() }}Решение:
- Nil pointer в slice:
items = []Closer{nil}→ panic приitem.Close(). - Nil pointer внутри non-nil interface:
items = []Closer{(*MyType)(nil)}→ еслиCloseобращается к ресиверу — panic. - Item с long-running Close: вызовы блокируются последовательно.
- Item Close() panic: убьёт цикл, остальные не закроются.
Защищённая версия:
func closeAll(items []Closer) { for _, item := range items { if item == nil { continue } func() { defer func() { _ = recover() }() _ = item.Close() }() }}8. Источники
Заголовок раздела «8. Источники»- Russ Cox — Go Data Structures: Interfaces — классический пост 2009, до сих пор актуален: https://research.swtch.com/interfaces
- Go source —
runtime/iface.go,runtime/runtime2.go(структурыiface,eface,itab). - Dave Cheney — The empty interface — https://dave.cheney.net/2018/09/19/the-empty-interface-said-nothing
- William Kennedy (Ardan Labs) — Interface mechanical sympathy — talks/blog с разбором internals.
- The Go Blog — Generics implementation: GC Shape Stenciling — https://go.dev/blog/intro-generics (полезно для понимания взаимодействия с интерфейсами).
- Habr 2024 — Внутренности интерфейсов в Go — серия статей с разбором ассемблера.
- Go Spec — Interface types — https://go.dev/ref/spec#Interface_types
- Go 1.20 release notes — секция про devirtualization.