Интерфейсы
Интерфейс в Go — это контракт + механизм полиморфизма + способ скрыть детали реализации. Структурная типизация (duck typing) делает интерфейсы Go непохожими на Java/C#. На собесе джуна про интерфейсы спрашивают чаще всего: что такое
iface/eface, как работает type assertion, и самое популярное — почемуvar err error = (*MyErr)(nil)не равно nil.
Содержание
Заголовок раздела «Содержание»- Базовое определение и API
- Внутреннее устройство (ПОД КАПОТОМ)
- Тонкие моменты / Gotchas
- Производительность / Memory considerations
- Типичные вопросы на собеседовании Junior
- Practice — мини-задачки
- Источники
1. Базовое определение и API
Заголовок раздела «1. Базовое определение и API»Объявление и реализация
Заголовок раздела «Объявление и реализация»type Greeter interface { Greet(name string) string}
type Friendly struct{}func (f Friendly) Greet(name string) string { return "Hello, " + name }
var g Greeter = Friendly{}fmt.Println(g.Greet("Anna")) // "Hello, Anna"⚠️ Никакого implements нет. Тип удовлетворяет интерфейс автоматически, если у него есть все методы из интерфейса. Это структурная типизация (duck typing).
Пустой интерфейс
Заголовок раздела «Пустой интерфейс»var x interface{} = "hello"x = 42x = []int{1, 2, 3}С Go 1.18 появился псевдоним any = interface{}:
var x any = "hello"Использовать any — рекомендуемый стиль, начиная с 1.18.
Type assertion
Заголовок раздела «Type assertion»var i any = "hello"s := i.(string) // strings, ok := i.(string) // безопасная проверкаn, ok := i.(int) // ok == falseЕсли без ok и тип не подходит → паника interface conversion: interface {} is string, not int.
Type switch
Заголовок раздела «Type switch»switch v := i.(type) {case string: fmt.Println("string:", v)case int: fmt.Println("int:", v)case nil: fmt.Println("nil")default: fmt.Println("unknown:", v)}Embedded interfaces
Заголовок раздела «Embedded interfaces»type Reader interface{ Read(p []byte) (int, error) }type Closer interface{ Close() error }
type ReadCloser interface { Reader Closer}Композиция интерфейсов — основа стандартной библиотеки (io.ReadCloser, io.ReadWriter, и т.д.).
Маленькие интерфейсы — Go-way
Заголовок раздела «Маленькие интерфейсы — Go-way»type Reader interface { Read(p []byte) (int, error) } // 1 методtype Writer interface { Write(p []byte) (int, error) } // 1 методtype Stringer interface { String() string } // 1 методtype Error interface { Error() string } // 1 методИдиома Go: интерфейсы маленькие, чаще 1-2 метода. Это даёт максимальную композицию и гибкость.
“The bigger the interface, the weaker the abstraction.” — Rob Pike
Кто объявляет интерфейс?
Заголовок раздела «Кто объявляет интерфейс?»В Go потребитель объявляет интерфейс, не производитель. Например, ваш код, использующий хранилище, объявляет:
type UserRepo interface { Get(id int64) (*User, error) Save(u *User) error}А конкретные реализации (postgres, mock) удовлетворяют его автоматически.
2. Внутреннее устройство (ПОД КАПОТОМ)
Заголовок раздела «2. Внутреннее устройство (ПОД КАПОТОМ)»iface и eface
Заголовок раздела «iface и eface»В Go runtime есть два представления интерфейса:
eface— для пустого интерфейса (interface{}/any).iface— для непустых интерфейсов (с методами).
Оба — два слова:
eface (empty interface):┌──────────┬──────────┐│ _type │ data ││ (*type) │ (*data) │└──────────┴──────────┘ 8 байт 8 байтiface (non-empty interface):┌──────────┬──────────┐│ itab │ data ││ (*itab) │ (*data) │└──────────┴──────────┘ 8 байт 8 байт_type— указатель на runtime type descriptor.itab— указатель на interface table (см. ниже).data— указатель на значение (если значение влезает в одно слово — иногда хранится прямо, но в современном Go всегда указатель).
itab — interface table
Заголовок раздела «itab — interface table»itab:┌─────────────────────┐│ inter (*interface) │ ← какой интерфейс│ _type (*type) │ ← конкретный тип│ hash (uint32) │ ← хэш типа│ _ [4]byte ││ fun [N]uintptr │ ← таблица методов (адреса функций)└─────────────────────┘fun — массив указателей на функции, по одному на каждый метод интерфейса. Когда вы вызываете g.Greet(name), runtime берёт itab.fun[0] и вызывает.
itab кэшируется в глобальной таблице itabTable, чтобы для пары (interface, concrete_type) itab вычислялся один раз.
Что хранится в data
Заголовок раздела «Что хранится в data»var g Greeter = Friendly{} // data = ptr to copy of Friendly{}В современном Go (с 1.4+) data всегда указатель. Это значит, что:
- Для маленьких значений (int) при упаковке в interface происходит аллокация в heap.
- Это часть escape analysis: помещение в interface → escape.
n := 42var i any = n // n boxes → копия n в heap, указатель в i.dataСоздание iface (упрощённый псевдокод)
Заголовок раздела «Создание iface (упрощённый псевдокод)»// var g Greeter = Friendly{x: 1}g.itab = lookup_itab(Greeter, Friendly) // взять/создать itabg.data = heap_alloc_and_copy(Friendly{x:1})Type assertion под капотом
Заголовок раздела «Type assertion под капотом»v, ok := i.(string)Runtime:
- Берёт
i._type(для eface) илиi.itab._type(для iface). - Сравнивает с дескриптором
string. - Если совпадает —
v = *(string*)i.data,ok = true. - Иначе —
v = "",ok = false.
Type switch — оптимизирован
Заголовок раздела «Type switch — оптимизирован»switch v := i.(type) {case A: ...case B: ...}Компилятор генерирует таблицу проверок по type hash; в худшем случае линейный проход по case’ам.
Method dispatch — это indirect call
Заголовок раздела «Method dispatch — это indirect call»Вызов метода через интерфейс — косвенный вызов через указатель из itab.fun. Это медленнее прямого вызова на 1-2 наносекунды, и препятствует inlining (как правило). Поэтому в hot path интерфейсы могут стать боттлнеком.
var g Greeter = Friendly{}g.Greet("X")// под капотом:// fn := g.itab.fun[0] // load// (*fn)(g.data, "X") // indirect callС Go 1.17+ часть таких вызовов devirtualizeable (если компилятор знает конкретный тип) → может inline.
Empty interface — особо
Заголовок раздела «Empty interface — особо»var x any = 42eface._type→ дескрипторint.eface.data→ указатель на heap-копию 42.
Type assertion x.(int):
- Проверяем
eface._type == &int_type_descriptor. - Если да —
*(int*)x.data.
nil interface
Заголовок раздела «nil interface»var i Greeterfmt.Println(i == nil) // trueКогда оба слова интерфейса (itab/_type, data) — нули.
НЕ-nil interface с nil data
Заголовок раздела «НЕ-nil interface с nil data»var p *MyError // nilvar err error = pfmt.Println(err == nil) // false!Под капотом:
err.itab = &itab{interface: error, type: *MyError, ...} ← НЕ nilerr.data = nilИнтерфейс err имеет тип *MyError, но значение nil. Сравнение err == nil смотрит, что оба слова — нули. itab ненулевой → err ≠ nil.
Это самый популярный gotcha на собесе! Подробнее в gotcha-секции.
3. Тонкие моменты / Gotchas
Заголовок раздела «3. Тонкие моменты / Gotchas»⚠️ Gotcha 1: nil interface vs typed-nil interface
Заголовок раздела «⚠️ Gotcha 1: nil interface vs typed-nil interface»type MyError struct{ Msg string }func (e *MyError) Error() string { return e.Msg }
func badFunc() error { var p *MyError // nil return p // <-- проблема}
func main() { err := badFunc() if err != nil { fmt.Println("error!") // печатается!!! }}Почему: err — это interface error, его itab указывает на (error, *MyError). Даже если data == nil, itab ≠ nil → err != nil.
Правильно:
func goodFunc() error { var p *MyError if shouldErr { p = &MyError{...} return p } return nil // явный nil interface}⚠️ Gotcha 2: Type assertion без ok — паника
Заголовок раздела «⚠️ Gotcha 2: Type assertion без ok — паника»var i any = "hi"n := i.(int) // panicВсегда используйте , ok форму, если не уверены.
⚠️ Gotcha 3: Method set и pointer/value receivers
Заголовок раздела «⚠️ Gotcha 3: Method set и pointer/value receivers»type Stringer interface{ String() string }
type T struct{}func (t *T) String() string { return "T" } // pointer receiver
var s Stringer = T{} // ❌ T не имеет String() (только *T)var s Stringer = &T{} // OKMethod set правило: T содержит только value methods. *T содержит и value, и pointer methods. Поэтому указатель удовлетворяет больше интерфейсов.
⚠️ Gotcha 4: Embedded interface в struct + nil
Заголовок раздела «⚠️ Gotcha 4: Embedded interface в struct + nil»type Logger interface{ Log(string) }type Service struct { Logger // встроенный интерфейс}
s := Service{} // Logger == nils.Log("hi") // panic: nil pointer dereference (или похожее)⚠️ Gotcha 5: interface{} как ключ map
Заголовок раздела «⚠️ Gotcha 5: interface{} как ключ map»m := map[any]int{}m[[]int{1,2}] = 1 // ❌ runtime panic: hash of unhashable type []intТолько comparable значения могут быть ключами. Если положили non-comparable в any-ключ — паника.
⚠️ Gotcha 6: Сравнение interface{}
Заголовок раздела «⚠️ Gotcha 6: Сравнение interface{}»var a any = []int{1, 2}var b any = []int{1, 2}fmt.Println(a == b) // panic: comparing uncomparable type []int== интерфейсов сравнивает типы и значения. Если значения не comparable → паника. Используйте reflect.DeepEqual для slice/map/etc.
⚠️ Gotcha 7: Огромный interface — антипаттерн
Заголовок раздела «⚠️ Gotcha 7: Огромный interface — антипаттерн»type Mega interface { A(); B(); C(); D(); E(); F(); // мочи всё подряд}- Сложно мокать.
- Сложно реализовать.
- Слабее композиция.
Лучше: маленькие интерфейсы + embed по необходимости.
⚠️ Gotcha 8: Stringer и %s — рекурсия
Заголовок раздела «⚠️ Gotcha 8: Stringer и %s — рекурсия»type T struct{ X int }func (t T) String() string { return fmt.Sprintf("%s", t) } // ♻️fmt.Sprintf("%s", t) вызывает t.String() → рекурсия → переполнение стека. Используйте %+v или поля напрямую.
⚠️ Gotcha 9: Передача через interface = аллокация (часто)
Заголовок раздела «⚠️ Gotcha 9: Передача через interface = аллокация (часто)»n := 42useAny(n) // n уезжает в heap, если useAny принимает anyВ hot path избегайте any для маленьких значений.
⚠️ Gotcha 10: Type switch — нет fall-through
Заголовок раздела «⚠️ Gotcha 10: Type switch — нет fall-through»switch v := i.(type) {case int: // fallthrough // ❌ ошибка компиляции в type switchcase string:}В type switch fallthrough запрещён.
⚠️ Gotcha 11: interface{}{}-параметры — потеря типа в generics
Заголовок раздела «⚠️ Gotcha 11: interface{}{}-параметры — потеря типа в generics»С 1.18+ generics часто заменяют any-параметры:
// До:func Print(v any) { ... }// После:func Print[T any](v T) { ... } // нет boxing⚠️ Gotcha 12: Stored nil в pkg.Err
Заголовок раздела «⚠️ Gotcha 12: Stored nil в pkg.Err»Если ваше API объявляет var ErrNotFound = errors.New("not found") и где-то перепутали тип — if err == ErrNotFound работает, но через wrapped errors нужно errors.Is(err, ErrNotFound).
4. Производительность / Memory considerations
Заголовок раздела «4. Производительность / Memory considerations»Аллокация при boxing
Заголовок раздела «Аллокация при boxing»var i any = 42 // alloc 8 байт (int) в heap, указатель в iЭто боксинг (boxing) — упаковка значения в interface. Происходит:
- При присваивании value type интерфейсу.
- При передаче в функцию, принимающую interface{}.
- При сохранении в
[]any,map[K]any,chan any.
Малые типы (int, bool) всегда требуют heap alloc при boxing’е (в Go 1.4+).
Indirect call vs direct call
Заголовок раздела «Indirect call vs direct call»| Вызов | Производительность |
|---|---|
Direct (f.M()) | Может быть inlined, ~1 ns |
Interface (i.M()) | Через itab.fun, не inline, ~3-5 ns |
В hot loops (миллионы итераций) разница значима. Профилируйте.
Devirtualization
Заголовок раздела «Devirtualization»Если компилятор может доказать конкретный тип за интерфейсом (через инлайн или escape analysis), он “девиртуализирует” вызов в прямой. Это случается, но не всегда.
itab cache
Заголовок раздела «itab cache»itab вычисляется один раз для пары (interface, concrete). После этого — lookup из глобальной hash-таблицы (быстрый).
Generics vs interface
Заголовок раздела «Generics vs interface»Generics (Go 1.18+) часто быстрее интерфейсов, потому что:
- Нет boxing’а.
- Компилятор может monomorphize/inline (для GCShape).
Но не всегда: для большого числа разных типов generics могут увеличить кодовую базу (GCShape ограничивает это).
eface vs iface — стоимость одинакова
Заголовок раздела «eface vs iface — стоимость одинакова»Оба — 2 слова, но iface имеет более жирный itab (с таблицей методов).
sync.Pool для interface storage
Заголовок раздела «sync.Pool для interface storage»Если делаете много boxing’ов одного типа в hot path — sync.Pool может помочь.
5. Типичные вопросы на собеседовании Junior
Заголовок раздела «5. Типичные вопросы на собеседовании Junior»-
Что такое интерфейс в Go? Тип-контракт: набор сигнатур методов. Любой тип, реализующий эти методы, автоматически удовлетворяет интерфейс (структурная типизация).
-
Чем отличается интерфейс в Go от Java? В Go нет ключевого слова
implements. Удовлетворение интерфейса проверяется компилятором по структуре методов. -
Что такое
interface{}/any? Пустой интерфейс — принимает любой тип.any— псевдонимinterface{}начиная с Go 1.18, идиоматично. -
Что такое iface и eface? Два внутренних представления interface:
iface(с itab) для непустых,eface(с *type) для пустых. Оба — 2 слова (16 байт на 64-битке). -
Что такое itab? Interface table: дескриптор пары (интерфейс, конкретный тип) + таблица указателей на методы. Создаётся один раз для пары, кэшируется.
-
Как работает type assertion
i.(T)? Сравнивает runtime-type интерфейса с T. Возвращает значение и bool ok (если двухзначная форма). Без ok → паника при несовпадении. -
Что такое type switch?
switch v := i.(type) { case T1: ...; case T2: ... }. Внутри каждого casevимеет соответствующий тип. -
Почему
var err error = (*MyErr)(nil); err != nil— true? У интерфейса два слова: type и value. err.type =*MyErr(не nil), err.value = nil. Interface == nil только когда оба слова — нули. -
Как правильно возвращать nil-error? Возвращать явно
return nil, а не nil-указатель типизированной ошибки. -
Что такое embedding интерфейсов? Включение одного интерфейса в другой.
io.ReadCloser = Reader + Closer. Идиоматичная композиция. -
Метод с pointer receiver — может ли value удовлетворить интерфейс? Нет. Только
*Tудовлетворяет.T— только методы с value receiver. -
Если значение лежит в interface, можно его модифицировать? Нельзя напрямую. Сначала type assertion в нужный тип (обычно указатель), потом мутация.
-
Что такое boxing в Go? Упаковка value-типа в интерфейс. Обычно вызывает heap allocation.
-
Дороги ли вызовы через интерфейс? Чуть дороже прямого вызова (indirect call через itab.fun), но обычно незначительно. Может препятствовать inlining.
-
Что лучше: interface{} или generics? Generics (Go 1.18+) предпочтительнее для типобезопасной обобщённой логики. interface{} оправдан для произвольного типа (например,
fmt.Println). -
Что такое маленький интерфейс? Почему так делают? Интерфейс с 1-2 методами. Идиома Go. Легко мокать, легко удовлетворить, легче композировать.
-
Может ли тип реализовывать несколько интерфейсов? Да, удовлетворение интерфейсов — независимо. Если есть все методы — удовлетворяет.
-
Что такое Stringer и Error интерфейсы?
Stringer { String() string }— для строкового представления (fmt).error { Error() string }— для ошибок. -
Что произойдёт при сравнении
==двух интерфейсов с не-comparable значениями? Паника во время выполнения. -
Какие типы НЕ comparable? Slice, map, function. Struct/array с такими полями — тоже не comparable.
-
Где в interface хранится значение?
dataполе — указатель на копию значения в heap (или прямо значение для редких единиц размером со слово в старом Go). -
Что такое method set? Набор методов типа. T содержит value methods, *T содержит value + pointer methods. Влияет на удовлетворение интерфейсов.
-
Полиморфизм в Go — как сделан? Через интерфейсы. Конкретные типы реализуют интерфейс, код работает через interface-переменные.
-
Зачем interface composition (embed)? Собирать большие интерфейсы из мелких без копирования кода. Удобно для сужения API.
-
Может ли интерфейс быть с приватными методами? Может (lowercase method). Тогда только типы из того же пакета могут его реализовать — это паттерн “sealed interface”.
6. Practice — мини-задачки
Заголовок раздела «6. Practice — мини-задачки»Задача 1
Заголовок раздела «Задача 1»type Animal interface{ Sound() string }type Dog struct{}func (d Dog) Sound() string { return "Woof" }
var a Animal = Dog{}fmt.Println(a.Sound())Ответ
`Woof`. Структурная типизация.Задача 2 — nil interface trap
Заголовок раздела «Задача 2 — nil interface trap»type MyErr struct{}func (e *MyErr) Error() string { return "err" }
func get() error { var e *MyErr return e}
func main() { err := get() if err != nil { fmt.Println("not nil") } else { fmt.Println("nil") }}Ответ
`not nil`. Классический gotcha. err имеет тип *MyErr (не nil), хотя значение nil.Задача 3
Заголовок раздела «Задача 3»var i any = 42n := i.(string)Ответ
Паника: `interface conversion: interface {} is int, not string`.Задача 4 — type switch
Заголовок раздела «Задача 4 — type switch»Напишите функцию describe(i any), которая печатает тип и значение для int, string, []byte, и неизвестного.
Ответ
```go func describe(i any) { switch v := i.(type) { case int: fmt.Println("int:", v) case string: fmt.Println("string:", v) case []byte: fmt.Println("bytes:", v) default: fmt.Printf("unknown %T: %v\n", v, v) } } ```Задача 5
Заголовок раздела «Задача 5»type S struct{}func (s *S) String() string { return "S" }
var i fmt.Stringer = S{} // ?Ответ
Ошибка компиляции: S не имеет метода String (только *S). Нужно `&S{}`.Задача 6
Заголовок раздела «Задача 6»type Reader interface{ Read([]byte) (int, error) }type Closer interface{ Close() error }type ReadCloser interface { Reader; Closer }
// Какие методы должен реализовать тип для удовлетворения ReadCloser?Ответ
`Read([]byte) (int, error)` и `Close() error`.Задача 7
Заголовок раздела «Задача 7»Сравнение двух any с разными типами:
var a any = 1var b any = "1"fmt.Println(a == b)Ответ
`false`. Разные типы → не равны.Задача 8 — Stringer для типа
Заголовок раздела «Задача 8 — Stringer для типа»type Money struct{ Cents int }// сделать так чтобы fmt.Println(Money{12345}) → "$123.45"Ответ
```go func (m Money) String() string { return fmt.Sprintf("$%d.%02d", m.Cents/100, m.Cents%100) } ```Задача 9
Заголовок раздела «Задача 9»Сколько байт занимает any на 64-битной системе?
Ответ
16 байт (две машинных слова: *type, *data).Задача 10
Заголовок раздела «Задача 10»Когда type assertion не паникует?
Ответ
Когда используется двухзначная форма: `v, ok := i.(T)`. Тогда при несовпадении `ok = false`, `v = zero value`.Задача 11
Заголовок раздела «Задача 11»Можно ли использовать map[any]int?
Ответ
Можно, но в ключи нельзя класть не-comparable значения (slice, map, func). Иначе runtime panic.Задача 12
Заголовок раздела «Задача 12»Что делает errors.Is?
Ответ
Проходит по цепочке wrapped errors через Unwrap() и сравнивает с target. Не использует == напрямую, поэтому работает с любыми обёртками.Задача 13
Заголовок раздела «Задача 13»Полиморфный список фигур:
type Shape interface{ Area() float64 }type Circle struct{ R float64 }type Square struct{ S float64 }Реализуйте Area() и посчитайте суммарную площадь []Shape.
Ответ
```go func (c Circle) Area() float64 { return math.Pi * c.R * c.R } func (s Square) Area() float64 { return s.S * s.S }shapes := []Shape{Circle{R:1}, Square{S:2}} total := 0.0 for _, s := range shapes { total += s.Area() }
</details>
### Задача 14Реализуйте mock через интерфейс:```gotype EmailSender interface { Send(to, body string) error }Напишите test mock.
Ответ
```go type mockSender struct { sent []string } func (m *mockSender) Send(to, body string) error { m.sent = append(m.sent, to) return nil } ```7. Источники
Заголовок раздела «7. Источники»- Go Spec — Interface types
- Russ Cox — “Go Data Structures: Interfaces” (классическая статья про iface/itab)
- The Go Blog — A Tour of Go’s Interfaces
- Effective Go — Interfaces
- Dave Cheney — “Interfaces in Go” series