Перейти к содержимому

Interfaces Internals — внутренности интерфейсов Go

Что это: углублённый разбор того, как интерфейсы устроены в runtime Go — структуры iface/eface, itab, кеш, dispatch. Зачем знать на Middle 1: на собеседовании обязательно спросят про nil-interface trap, про cost dynamic dispatch и про то, что внутри пары “type + value”. Без понимания исходников runtime/iface.go ответ будет поверхностным.


  1. Базовая концепция (краткое повторение)
  2. Под капотом: iface, eface, itab — как это устроено в runtime
  3. Тонкие моменты / Gotchas (12+)
  4. Производительность и compiler optimizations
  5. Когда использовать / когда НЕ использовать
  6. Вопросы на собесе Middle 1 (25+)
  7. Practice — задачи (7)
  8. Источники

Интерфейс в 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 байт.


В исходниках 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-битной платформе.

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):

  1. Компилятор знает, что Read — это первый метод интерфейса (индекс 0).
  2. Генерируется ассемблер: загрузить r.tab.fun[0], вызвать его с аргументами (r.data, buf).
  3. Метод получает свой ресивер через r.data (указатель на os.File).

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) lookup

Когда пишем:

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) аллокации может не быть, используется кеш. Но для других значений — будет.

v, ok := r.(*os.File) // type assertion с проверкой
v := r.(*os.File) // type assertion с panic при несовпадении

В runtime есть несколько функций:

ФункцияКогда вызывается
assertI2Iinterface → interface (с методами)
assertE2Iempty interface → interface (с методами)
assertI2Tinterface → concrete type
assertE2Tempty interface → concrete type
typeAssertGeneric версия (Go 1.18+)

FAST path для assertE2T (eface → concrete):

if eface._type == target_type {
return eface.data, true
}
return zero, false

SLOW path для assertI2I (iface → другой интерфейс):

if iface.tab.inter == target_interface {
return iface, true // тот же интерфейс
}
// Нужно создать новый itab — вызвать getitab
newtab := getitab(target_interface, iface.tab._type, true)
if newtab == nil {
return zero, false
}
return iface{tab: newtab, data: iface.data}, true

Поэтому конверсия между разными интерфейсами дороже, чем между concrete type и интерфейсом.

switch v := r.(type) {
case *os.File:
// ...
case *bytes.Buffer:
// ...
default:
}

Компилятор генерирует что-то вроде:

hash := r.tab._type.hash // или r._type.hash для eface
switch 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) при бинарном поиске.

Это редко возможно. Компилятор обычно не может инлайнить вызов через интерфейс, потому что не знает конкретный тип в compile-time.

Исключение — devirtualization (Go 1.20+ улучшил это):

var r io.Reader = &bytes.Buffer{...}
n, _ := r.Read(buf) // компилятор может догадаться: r — это *bytes.Buffer → инлайн

Devirtualization работает только в простых случаях, когда escape analysis видит, что значение интерфейса не покидает функцию.

Замер на современном x86_64 (Go 1.22, без MFENCE):

Тип вызоваns/opInline?
Direct call~0.3 nsyes
Interface call~1.5 nsrarely
Reflect call~150 nsno

Overhead интерфейса — это:

  1. Загрузка iface.tab.fun[i] (одна indirection).
  2. Indirect call (branch predictor может ошибиться).
  3. Блокировка inlining → нельзя оптимизировать аргументы, escape, и т.д.

На горячем пути (10^9 вызовов) разница может быть существенной. На холодном — не имеет значения.


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) — работает.

Часто пишут:

var _ io.Reader = (*MyReader)(nil)

Эта строка ничего не делает в runtime, но в compile-time проверяет, что *MyReader удовлетворяет io.Reader. Если нет — ошибка компиляции.

Полезно для:

  • Документации (явно показано, что тип реализует интерфейс).
  • Раннего обнаружения breaking changes (если интерфейс изменился).
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 Animal
var a Animal = &Dog{} // ← ок

Правило: method set типа T содержит только методы с value receiver. Method set типа *T содержит и value, и pointer receiver.

Поэтому:

  • Dog — не удовлетворяет Animal (нет Sound).
  • *Dog — удовлетворяет.

Это касается ТОЛЬКО автоматических преобразований через интерфейс. Прямой вызов Dog{}.Sound() работает (Go автоматически берёт адрес).

type A interface { Foo() }
type B interface { Foo() } // тот же Foo
type 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) это компилировалось, но было невозможно реализовать. Сейчас — явная ошибка.

func newReader() (r io.Reader) {
r = &MyReader{} // ← escape to heap!
return
}

Здесь &MyReader{} обязательно убегает в heap, потому что:

  1. Кладётся в интерфейс.
  2. Возвращается из функции.

Компилятор не может заинлайнить и оставить на стеке.

Gotcha 7: сравнение интерфейсов — может паниковать

Заголовок раздела «Gotcha 7: сравнение интерфейсов — может паниковать»
var a, b any
a = []int{1, 2, 3}
b = []int{1, 2, 3}
fmt.Println(a == b) // ← panic: runtime error: comparing uncomparable type []int

Интерфейсы сравниваются как:

  1. (tab_a == tab_b) && (data_a == data_b) — если тип в _type comparable, идёт глубокое сравнение.
  2. Если тип не 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) // ok
b := []byte(x.(string)) // нужно явно: any → string → []byte

Прямого x.([]byte) не получится, если x хранит string. Все type assertions — строго по конкретному типу.

type T struct { x int }
var t *T // nil pointer
var i any = t // i — non-nil interface holding nil
fmt.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()
}
m := map[any]int{}
m["foo"] = 1
m[42] = 2
m[[]int{1,2,3}] = 3 // ← panic при вставке (uncomparable type)

Map допускает interface как key, но во время вставки проверяется comparability. Если не comparable — panic.

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 — не разворачивает.


Operation ns/op Notes
-------------------------------------------------------------------
Direct method call (concrete type) 0.3 inlined
Interface method call 1.5 one indirection
Type assertion (FAST path, success) 0.8 pointer compare
Type assertion (SLOW path, with itab) 3.0 hash lookup
Type assertion (failure, with ok) 0.6
Type switch (3 cases) 1.5 hash + compare
convT2E (interface{}-pack with alloc) 15-30 heap allocation
convT2I (interface-pack with itab cache) 5 if cached

В Go 1.20+ улучшен анализ. Можно девиртуализировать, если:

  1. Интерфейс объявлен и присвоен в одном функционале (без выхода в global).
  2. Escape analysis не “теряет” значение.
  3. Конкретный тип единственный и известен в 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.Buffer
func bad(x int) any {
return x // ← heap allocation!
}
func good(x *int) any {
return x // ← без allocation (указатель копируется)
}

Scalar boxing в any всегда аллоцирует (кроме малых интов). Поэтому избегайте на горячем пути.

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). Принимайте интерфейс (узкий), возвращайте конкретный тип (точный).


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…). При вызове:

  1. Получить iface.tab.fun[i] — указатель на метод конкретного типа.
  2. Indirect call с аргументом iface.data как ресивер.
  3. Метод выполняется, получая 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 *int
var x any = p
fmt.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).


Что выведет?

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 напрямую.


Что выведет?

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. Не удовлетворяет.


Реализуйте функцию 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) → true
  • IsNil((*int)(nil)) → true
  • IsNil(42) → false
  • IsNil("") → false (пустая строка — не nil)

Что выведет?

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 — была бы общая ссылка.


Напишите 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.

Что выведет?

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.


Найдите все способы сломать этот код:

type Closer interface { Close() error }
func closeAll(items []Closer) {
for _, item := range items {
item.Close()
}
}

Решение:

  1. Nil pointer в slice: items = []Closer{nil} → panic при item.Close().
  2. Nil pointer внутри non-nil interface: items = []Closer{(*MyType)(nil)} → если Close обращается к ресиверу — panic.
  3. Item с long-running Close: вызовы блокируются последовательно.
  4. Item Close() panic: убьёт цикл, остальные не закроются.

Защищённая версия:

func closeAll(items []Closer) {
for _, item := range items {
if item == nil {
continue
}
func() {
defer func() {
_ = recover()
}()
_ = item.Close()
}()
}
}

  1. Russ CoxGo Data Structures: Interfaces — классический пост 2009, до сих пор актуален: https://research.swtch.com/interfaces
  2. Go sourceruntime/iface.go, runtime/runtime2.go (структуры iface, eface, itab).
  3. Dave CheneyThe empty interfacehttps://dave.cheney.net/2018/09/19/the-empty-interface-said-nothing
  4. William Kennedy (Ardan Labs)Interface mechanical sympathy — talks/blog с разбором internals.
  5. The Go BlogGenerics implementation: GC Shape Stencilinghttps://go.dev/blog/intro-generics (полезно для понимания взаимодействия с интерфейсами).
  6. Habr 2024Внутренности интерфейсов в Go — серия статей с разбором ассемблера.
  7. Go SpecInterface typeshttps://go.dev/ref/spec#Interface_types
  8. Go 1.20 release notes — секция про devirtualization.