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

Интерфейсы

Интерфейс в Go — это контракт + механизм полиморфизма + способ скрыть детали реализации. Структурная типизация (duck typing) делает интерфейсы Go непохожими на Java/C#. На собесе джуна про интерфейсы спрашивают чаще всего: что такое iface/eface, как работает type assertion, и самое популярное — почему var err error = (*MyErr)(nil) не равно nil.

  1. Базовое определение и API
  2. Внутреннее устройство (ПОД КАПОТОМ)
  3. Тонкие моменты / Gotchas
  4. Производительность / Memory considerations
  5. Типичные вопросы на собеседовании Junior
  6. Practice — мини-задачки
  7. Источники

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 = 42
x = []int{1, 2, 3}

С Go 1.18 появился псевдоним any = interface{}:

var x any = "hello"

Использовать any — рекомендуемый стиль, начиная с 1.18.

var i any = "hello"
s := i.(string) // string
s, ok := i.(string) // безопасная проверка
n, ok := i.(int) // ok == false

Если без ok и тип не подходит → паника interface conversion: interface {} is string, not int.

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)
}
type Reader interface{ Read(p []byte) (int, error) }
type Closer interface{ Close() error }
type ReadCloser interface {
Reader
Closer
}

Композиция интерфейсов — основа стандартной библиотеки (io.ReadCloser, io.ReadWriter, и т.д.).

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) удовлетворяют его автоматически.


В Go runtime есть два представления интерфейса:

  1. eface — для пустого интерфейса (interface{} / any).
  2. 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:
┌─────────────────────┐
│ 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 вычислялся один раз.

var g Greeter = Friendly{} // data = ptr to copy of Friendly{}

В современном Go (с 1.4+) data всегда указатель. Это значит, что:

  • Для маленьких значений (int) при упаковке в interface происходит аллокация в heap.
  • Это часть escape analysis: помещение в interface → escape.
n := 42
var i any = n // n boxes → копия n в heap, указатель в i.data
// var g Greeter = Friendly{x: 1}
g.itab = lookup_itab(Greeter, Friendly) // взять/создать itab
g.data = heap_alloc_and_copy(Friendly{x:1})
v, ok := i.(string)

Runtime:

  1. Берёт i._type (для eface) или i.itab._type (для iface).
  2. Сравнивает с дескриптором string.
  3. Если совпадает — v = *(string*)i.data, ok = true.
  4. Иначе — v = "", ok = false.
switch v := i.(type) {
case A: ...
case B: ...
}

Компилятор генерирует таблицу проверок по type hash; в худшем случае линейный проход по case’ам.

Вызов метода через интерфейс — косвенный вызов через указатель из 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.

var x any = 42
  • eface._type → дескриптор int.
  • eface.data → указатель на heap-копию 42.

Type assertion x.(int):

  • Проверяем eface._type == &int_type_descriptor.
  • Если да — *(int*)x.data.
var i Greeter
fmt.Println(i == nil) // true

Когда оба слова интерфейса (itab/_type, data) — нули.

var p *MyError // nil
var err error = p
fmt.Println(err == nil) // false!

Под капотом:

err.itab = &itab{interface: error, type: *MyError, ...} ← НЕ nil
err.data = nil

Интерфейс err имеет тип *MyError, но значение nil. Сравнение err == nil смотрит, что оба слова — нули. itab ненулевой → err ≠ nil.

Это самый популярный gotcha на собесе! Подробнее в gotcha-секции.


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
}
var i any = "hi"
n := i.(int) // panic

Всегда используйте , ok форму, если не уверены.

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{} // OK

Method set правило: T содержит только value methods. *T содержит и value, и pointer methods. Поэтому указатель удовлетворяет больше интерфейсов.

type Logger interface{ Log(string) }
type Service struct {
Logger // встроенный интерфейс
}
s := Service{} // Logger == nil
s.Log("hi") // panic: nil pointer dereference (или похожее)
m := map[any]int{}
m[[]int{1,2}] = 1 // ❌ runtime panic: hash of unhashable type []int

Только comparable значения могут быть ключами. Если положили non-comparable в any-ключ — паника.

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.

type Mega interface {
A(); B(); C(); D(); E(); F(); // мочи всё подряд
}
  • Сложно мокать.
  • Сложно реализовать.
  • Слабее композиция.

Лучше: маленькие интерфейсы + embed по необходимости.

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 := 42
useAny(n) // n уезжает в heap, если useAny принимает any

В hot path избегайте any для маленьких значений.

switch v := i.(type) {
case int:
// fallthrough // ❌ ошибка компиляции в type switch
case 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

Если ваше API объявляет var ErrNotFound = errors.New("not found") и где-то перепутали тип — if err == ErrNotFound работает, но через wrapped errors нужно errors.Is(err, ErrNotFound).


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+).

ВызовПроизводительность
Direct (f.M())Может быть inlined, ~1 ns
Interface (i.M())Через itab.fun, не inline, ~3-5 ns

В hot loops (миллионы итераций) разница значима. Профилируйте.

Если компилятор может доказать конкретный тип за интерфейсом (через инлайн или escape analysis), он “девиртуализирует” вызов в прямой. Это случается, но не всегда.

itab вычисляется один раз для пары (interface, concrete). После этого — lookup из глобальной hash-таблицы (быстрый).

Generics (Go 1.18+) часто быстрее интерфейсов, потому что:

  • Нет boxing’а.
  • Компилятор может monomorphize/inline (для GCShape).

Но не всегда: для большого числа разных типов generics могут увеличить кодовую базу (GCShape ограничивает это).

Оба — 2 слова, но iface имеет более жирный itab (с таблицей методов).

Если делаете много boxing’ов одного типа в hot path — sync.Pool может помочь.


  1. Что такое интерфейс в Go? Тип-контракт: набор сигнатур методов. Любой тип, реализующий эти методы, автоматически удовлетворяет интерфейс (структурная типизация).

  2. Чем отличается интерфейс в Go от Java? В Go нет ключевого слова implements. Удовлетворение интерфейса проверяется компилятором по структуре методов.

  3. Что такое interface{} / any? Пустой интерфейс — принимает любой тип. any — псевдоним interface{} начиная с Go 1.18, идиоматично.

  4. Что такое iface и eface? Два внутренних представления interface: iface (с itab) для непустых, eface (с *type) для пустых. Оба — 2 слова (16 байт на 64-битке).

  5. Что такое itab? Interface table: дескриптор пары (интерфейс, конкретный тип) + таблица указателей на методы. Создаётся один раз для пары, кэшируется.

  6. Как работает type assertion i.(T)? Сравнивает runtime-type интерфейса с T. Возвращает значение и bool ok (если двухзначная форма). Без ok → паника при несовпадении.

  7. Что такое type switch? switch v := i.(type) { case T1: ...; case T2: ... }. Внутри каждого case v имеет соответствующий тип.

  8. Почему var err error = (*MyErr)(nil); err != nil — true? У интерфейса два слова: type и value. err.type = *MyErr (не nil), err.value = nil. Interface == nil только когда оба слова — нули.

  9. Как правильно возвращать nil-error? Возвращать явно return nil, а не nil-указатель типизированной ошибки.

  10. Что такое embedding интерфейсов? Включение одного интерфейса в другой. io.ReadCloser = Reader + Closer. Идиоматичная композиция.

  11. Метод с pointer receiver — может ли value удовлетворить интерфейс? Нет. Только *T удовлетворяет. T — только методы с value receiver.

  12. Если значение лежит в interface, можно его модифицировать? Нельзя напрямую. Сначала type assertion в нужный тип (обычно указатель), потом мутация.

  13. Что такое boxing в Go? Упаковка value-типа в интерфейс. Обычно вызывает heap allocation.

  14. Дороги ли вызовы через интерфейс? Чуть дороже прямого вызова (indirect call через itab.fun), но обычно незначительно. Может препятствовать inlining.

  15. Что лучше: interface{} или generics? Generics (Go 1.18+) предпочтительнее для типобезопасной обобщённой логики. interface{} оправдан для произвольного типа (например, fmt.Println).

  16. Что такое маленький интерфейс? Почему так делают? Интерфейс с 1-2 методами. Идиома Go. Легко мокать, легко удовлетворить, легче композировать.

  17. Может ли тип реализовывать несколько интерфейсов? Да, удовлетворение интерфейсов — независимо. Если есть все методы — удовлетворяет.

  18. Что такое Stringer и Error интерфейсы? Stringer { String() string } — для строкового представления (fmt). error { Error() string } — для ошибок.

  19. Что произойдёт при сравнении == двух интерфейсов с не-comparable значениями? Паника во время выполнения.

  20. Какие типы НЕ comparable? Slice, map, function. Struct/array с такими полями — тоже не comparable.

  21. Где в interface хранится значение? data поле — указатель на копию значения в heap (или прямо значение для редких единиц размером со слово в старом Go).

  22. Что такое method set? Набор методов типа. T содержит value methods, *T содержит value + pointer methods. Влияет на удовлетворение интерфейсов.

  23. Полиморфизм в Go — как сделан? Через интерфейсы. Конкретные типы реализуют интерфейс, код работает через interface-переменные.

  24. Зачем interface composition (embed)? Собирать большие интерфейсы из мелких без копирования кода. Удобно для сужения API.

  25. Может ли интерфейс быть с приватными методами? Может (lowercase method). Тогда только типы из того же пакета могут его реализовать — это паттерн “sealed interface”.


type Animal interface{ Sound() string }
type Dog struct{}
func (d Dog) Sound() string { return "Woof" }
var a Animal = Dog{}
fmt.Println(a.Sound())
Ответ `Woof`. Структурная типизация.
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.
var i any = 42
n := i.(string)
Ответ Паника: `interface conversion: interface {} is int, not string`.

Напишите функцию 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) } } ```
type S struct{}
func (s *S) String() string { return "S" }
var i fmt.Stringer = S{} // ?
Ответ Ошибка компиляции: S не имеет метода String (только *S). Нужно `&S{}`.
type Reader interface{ Read([]byte) (int, error) }
type Closer interface{ Close() error }
type ReadCloser interface { Reader; Closer }
// Какие методы должен реализовать тип для удовлетворения ReadCloser?
Ответ `Read([]byte) (int, error)` и `Close() error`.

Сравнение двух any с разными типами:

var a any = 1
var b any = "1"
fmt.Println(a == b)
Ответ `false`. Разные типы → не равны.
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) } ```

Сколько байт занимает any на 64-битной системе?

Ответ 16 байт (две машинных слова: *type, *data).

Когда type assertion не паникует?

Ответ Когда используется двухзначная форма: `v, ok := i.(T)`. Тогда при несовпадении `ok = false`, `v = zero value`.

Можно ли использовать map[any]int?

Ответ Можно, но в ключи нельзя класть не-comparable значения (slice, map, func). Иначе runtime panic.

Что делает errors.Is?

Ответ Проходит по цепочке wrapped errors через Unwrap() и сравнивает с target. Не использует == напрямую, поэтому работает с любыми обёртками.

Полиморфный список фигур:

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 через интерфейс:
```go
type 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 } ```

  1. Go Spec — Interface types
  2. Russ Cox — “Go Data Structures: Interfaces” (классическая статья про iface/itab)
  3. The Go Blog — A Tour of Go’s Interfaces
  4. Effective Go — Interfaces
  5. Dave Cheney — “Interfaces in Go” series