Skip to content

Interface Internals

Интерфейсы в Go — это не просто контракты. Это сложные runtime-структуры с vtable-подобным dispatch, кешированием itab и умным boxing. В этой статье разберём, как именно компилятор и runtime представляют интерфейсы в памяти.

TL;DR

СтруктураРазмерНазначение
iface16 байт (2 указателя, 64-bit)Интерфейс с методами
eface16 байт (2 указателя, 64-bit)Пустой интерфейс any / interface{}
itab32+ байтаТаблица методов + метаданные типов
_type~48 байт (порядок, 64-bit)Type descriptor (размер, hash, методы)
ОперацияСтоимость
Interface assignmentO(1), возможна аллокация для boxing
Method dispatchIndirect call через itab.fun[] (обычно единицы ns)
Type assertion (попадание в кеш)O(1) в среднем, проверка указателя itab
Type assertion (промах кеша)O(1) в среднем, O(n) в худшем случае

iface vs eface

Go использует две разные структуры для представления интерфейсов:

iface — интерфейс с методами          eface — пустой интерфейс (any)
┌──────────────────────┐              ┌──────────────────────┐
│  tab  *itab          │──────┐       │  _type *_type        │──────┐
├──────────────────────┤      │       ├──────────────────────┤      │
│  data unsafe.Pointer │──┐   │       │  data  unsafe.Pointer│──┐   │
└──────────────────────┘  │   │       └──────────────────────┘  │   │
                          │   │                                 │   │
       ┌──────────────────┘   │            ┌────────────────────┘   │
       │                      │            │                        │
       ▼                      ▼            ▼                        ▼
   ┌───────┐           ┌───────────┐   ┌───────┐            ┌───────────┐
   │ value │           │   itab    │   │ value │            │   _type   │
   │ (T)   │           │  (методы) │   │ (T)   │            │(type info)│
   └───────┘           └───────────┘   └───────┘            └───────────┘

Почему две структуры?

  • iface хранит *itab — таблицу методов для конкретной пары (interface type, concrete type)
  • eface не имеет методов, поэтому хранит только *_type — информацию о типе значения
  • Оптимизация: для any не нужно создавать itab
go
// runtime/runtime2.go

// Интерфейс с методами: io.Reader, fmt.Stringer, etc.
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

// Пустой интерфейс: any, interface{}
type eface struct {
    _type *_type
    data  unsafe.Pointer
}

Когда какая используется

go
var w io.Writer = os.Stdout  // iface: есть метод Write
var r io.Reader = f          // iface: есть метод Read
var s fmt.Stringer = t       // iface: есть метод String

var x any = 42               // eface: нет методов
var y interface{} = "hello"  // eface: нет методов

Разница определяется типом слева. Один и тот же объект можно упаковать по-разному:

go
var buf bytes.Buffer

var a any = &buf      // eface: только _type + data
var b io.Writer = &buf // iface: tab + data

iface нужен для методов (адреса лежат в itab), eface — без методов.

iface Memory Layout

Рассмотрим подробнее структуру iface:

Stack                        Heap / Static
┌─────────────────────┐
│     var w Writer    │
│  ┌───────────────┐  │      ┌─────────────────────────┐
│  │ tab  *itab    │──┼─────▶│ itab                    │
│  ├───────────────┤  │      │ ┌─────────────────────┐ │
│  │ data *T       │──┼──┐   │ │ inter *interfacetype│ │───▶ Writer type info
│  └───────────────┘  │  │   │ ├─────────────────────┤ │
└─────────────────────┘  │   │ │ _type *_type        │ │───▶ *os.File type info
                         │   │ ├─────────────────────┤ │
                         │   │ │ hash  uint32        │ │
                         │   │ ├─────────────────────┤ │
                         │   │ │ fun   [1]uintptr    │ │───▶ (*os.File).Write
                         │   │ └─────────────────────┘ │
                         │   └─────────────────────────┘

                         │   ┌────────────────────────┐
                         └──▶│ *os.File value         │
                             │ (конкретное значение)  │
                             └────────────────────────┘

Поле data

data указывает на конкретное значение. Важные нюансы:

go
// 1. Указатель — data содержит сам указатель (без дополнительной аллокации)
var w io.Writer = &Buffer{}  // data = адрес Buffer на heap

// 2. Маленькое значение (≤ pointer size) — может быть inline
var s fmt.Stringer = mySmallStruct{}  // data может содержать само значение

// 3. Большое значение — аллокация на heap
var s fmt.Stringer = myBigStruct{}  // data = указатель на копию в heap

Важно: интерфейс хранит копию значения, а не ссылку (если это не pointer).

go
type S struct{ n int }
s := S{n: 1}
var i any = s
s.n = 2
fmt.Println(i.(S).n) // 1: в интерфейсе лежит копия

Поэтому присваивание в интерфейс может приводить к аллокации: нужна копия.

itab Deep Dive

itab — ключевая структура для method dispatch:

go
// internal/abi/iface.go
type ITab struct {
    Inter *InterfaceType  // описание интерфейса
    Type  *Type           // описание конкретного типа
    Hash  uint32          // копия Type.Hash для быстрого сравнения
    _     [4]byte         // padding
    Fun   [1]uintptr      // variable-size array указателей на методы
}

Структура itab в памяти

itab для (io.Writer, *os.File)
┌─────────────────────────────────────────────────────────┐
│ Inter  ───────────▶ interfacetype {                     │
│                       Type: _type для io.Writer         │
│                       PkgPath: "io"                     │
│                       Methods: [{Name: "Write", ...}]   │
│                     }                                   │
├─────────────────────────────────────────────────────────┤
│ Type   ───────────▶ _type {                             │
│                       Size: 8                           │
│                       Hash: 0x12345678                  │
│                       Kind: Ptr                         │
│                       ...                               │
│                     }                                   │
├─────────────────────────────────────────────────────────┤
│ Hash   = 0x12345678   (копия Type.Hash)                 │
├─────────────────────────────────────────────────────────┤
│ Fun[0] = (*os.File).Write   ◀── метод #0                │
│ Fun[1] = ...                    (если бы были другие)   │
│ ...                                                     │
└─────────────────────────────────────────────────────────┘

Fun[] — массив указателей на методы

Размер Fun[] определяется количеством методов в интерфейсе:

go
// io.Writer имеет 1 метод → Fun[1]
type Writer interface {
    Write(p []byte) (n int, err error)
}

// io.ReadWriter имеет 2 метода → Fun[2]
type ReadWriter interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
}

// io.ReadWriteCloser имеет 3 метода → Fun[3]
type ReadWriteCloser interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
}

Компилятор при вызове w.Write заранее знает индекс метода в интерфейсе и генерирует обращение к itab.Fun[idx]. Поэтому порядок методов фиксируется на этапе компиляции и одинаков для всех типов.

Порядок методов в Fun[] определяется алфавитным порядком имён методов:

go
// io.ReadWriteCloser
// Fun[0] = Close  (C < R < W)
// Fun[1] = Read
// Fun[2] = Write

itab Caching

Создание itab требует поиска методов — O(n×m) операция. Go кеширует все созданные itab в глобальной hash table.

Почему это важно: повторный поиск методов дорог, а itab переиспользуется для пары (interface type, concrete type).

itab lookup flow
┌──────────────┐
│ getitab()    │
└──────┬───────┘
       │ hash

  [таблица itab]
   │ hit? │
   ├──────┼───────────────┐
   │ yes  │ return itab   │
   │ no   │ createItab()  │
   └──────┴───────┬───────┘
                  │ insert

              return itab

Hash Table структура

go
// runtime/iface.go
const itabInitSize = 512

// Глобальная таблица itab
var (
    itabLock mutex
    itabTable = &itabTableType{
        size:    itabInitSize,
        count:   0,
        entries: make([]unsafe.Pointer, itabInitSize),
    }
)
itab Hash Table
┌────────────────────────────────────────────────────────────┐
│ size: 512    count: 47                                     │
├────────────────────────────────────────────────────────────┤
│ entries[]                                                  │
│ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐  │
│ │  0   │  1   │  2   │  3   │  4   │  5   │ ...  │ 511  │  │
│ │ nil  │ itab │ nil  │ itab │ nil  │ itab │      │ nil  │  │
│ │      │  ↓   │      │  ↓   │      │  ↓   │      │      │  │
│ └──────┴──┼───┴──────┴──┼───┴──────┴──┼───┴──────┴──────┘  │
│           │             │             │                    │
│           ▼             ▼             ▼                    │
│       ┌───────┐     ┌───────┐     ┌───────┐                │
│       │ itab  │     │ itab  │     │ itab  │                │
│       │Writer │     │Reader │     │Stringer                │
│       │*File  │     │*Buffer│     │myType │                │
│       └───────┘     └───────┘     └───────┘                │
└────────────────────────────────────────────────────────────┘

Lookup алгоритм

go
// runtime/iface.go (упрощённо)
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    // Вычисляем hash из пары (inter, typ)
    h := itabHashFunc(inter, typ) % len(itabTable.entries)

    // Ищем в таблице (linear probing при коллизиях)
    for {
        p := atomic.Loadp(&itabTable.entries[h])
        if p == nil {
            break // не найден
        }
        t := (*itab)(p)
        if t.Inter == inter && t.Type == typ {
            return t // найден!
        }
        h = (h + 1) % len(itabTable.entries)
    }

    // Не найден — создаём новый
    return createItab(inter, typ)
}

Упрощённо:

  1. Посчитать hash пары (interface type, concrete type)
  2. Linear probing при коллизиях
  3. Если не найдено → создать itab

Средний случай — O(1), потому что таблица держится с невысокой загрузкой.

Создание itab

go
func createItab(inter *interfacetype, typ *_type) *itab {
    // 1. Аллоцируем itab (размер зависит от количества методов)
    m := &itab{
        Inter: inter,
        Type:  typ,
        Hash:  typ.Hash,
    }

    // 2. Заполняем Fun[] — ищем методы типа по именам из интерфейса
    methods := typ.Methods()
    for i, im := range inter.Methods {
        // Бинарный поиск метода в отсортированном списке методов типа
        j := sort.Search(len(methods), func(j int) bool {
            return methods[j].Name >= im.Name
        })
        if j < len(methods) && methods[j].Name == im.Name {
            m.Fun[i] = methods[j].Fn
        } else {
            // Метод не найден — тип не реализует интерфейс
            return nil
        }
    }

    // 3. Добавляем в кеш
    itabAdd(m)

    return m
}

На этом шаге происходит проверка реализации интерфейса: если метод отсутствует, создание itab завершается ошибкой. Это то место, где runtime решает, что тип не реализует интерфейс при assertion или при приведении.

itab Table Growth

При заполнении таблицы на ~75% происходит рост:

go
func itabAdd(m *itab) {
    if itabTable.count >= itabTable.size * 3 / 4 {
        // Удваиваем размер таблицы
        grow := itabTable.size * 2
        newEntries := make([]unsafe.Pointer, grow)

        // Рехешируем все существующие itab
        for _, p := range itabTable.entries {
            if p != nil {
                h := itabHashFunc((*itab)(p).Inter, (*itab)(p).Type) % grow
                // ... вставляем в новую позицию
            }
        }

        itabTable.entries = newEntries
        itabTable.size = grow
    }

    // Вставляем новый itab
    h := itabHashFunc(m.Inter, m.Type) % itabTable.size
    // ... linear probing для поиска свободного слота
    atomic.StorepNoWB(&itabTable.entries[h], unsafe.Pointer(m))
    itabTable.count++
}

Рост уменьшает коллизии и держит поиск в среднем O(1); дорогие вставки только при расширении.

_type Structure (Type Descriptor)

_type содержит всю информацию о типе, необходимую runtime:

go
// internal/abi/type.go
type Type struct {
    Size_       uintptr   // размер значения типа в байтах
    PtrBytes    uintptr   // размер prefix с указателями
    Hash        uint32    // hash типа для быстрого сравнения
    TFlag       TFlag     // флаги (extraStar, named, etc.)
    Align_      uint8     // выравнивание значения
    FieldAlign_ uint8     // выравнивание поля структуры
    Kind_       Kind      // вид типа (Int, Struct, Ptr, etc.)
    // ...
}

Ключевые поля:

  • Size_, Align_, FieldAlign_ — размещение и копирование
  • PtrBytes — сколько байт содержат указатели (важно для GC)
  • Hash, Kind_ — сравнение, type switch, reflect

Type Kinds

go
const (
    Invalid Kind = iota
    Bool
    Int
    Int8
    Int16
    Int32
    Int64
    Uint
    // ... числовые типы
    String
    UnsafePointer
    Array
    Chan
    Func
    Interface
    Map
    Pointer
    Slice
    Struct
)

Kind — быстрый категоризатор типа для ветвлений runtime.

_type для разных типов

_type для int                    _type для *os.File
┌─────────────────────┐          ┌─────────────────────┐
│ Size_: 8            │          │ Size_: 8            │
│ Hash:  0x...        │          │ Hash:  0x...        │
│ Kind_: Int          │          │ Kind_: Pointer      │
│ Align_: 8           │          │ Elem:  ───────────▶ _type для os.File
└─────────────────────┘          └─────────────────────┘

_type для []byte                 _type для struct{x int; y string}
┌─────────────────────┐          ┌──────────────────────────────────┐
│ Size_: 24           │          │ Size_: 24                        │
│ Kind_: Slice        │          │ Kind_: Struct                    │
│ Elem:  ───────────▶ │          │ Fields: [                        │
│   _type для uint8   │          │   {Name: "x", Type: *int, ...}   │
└─────────────────────┘          │   {Name: "y", Type: *string, ...}│
                                 │ ]                                │
                                 └──────────────────────────────────┘

Составные типы ссылаются на другие _type:

  • Pointer хранит ссылку на Elem
  • Slice хранит Elem и размер заголовка
  • Struct хранит массив полей (имя, тип, offset)

Эта информация нужна для reflect, сравнения и GC‑сканирования.

Interface Value Boxing

При присваивании значения интерфейсу происходит "boxing" — упаковка значения:

convT функции

Runtime имеет оптимизированные функции для boxing распространённых типов:

go
// runtime/iface.go
func convT(t *_type, v unsafe.Pointer) unsafe.Pointer  // generic
func convTnoptr(t *_type, v unsafe.Pointer) unsafe.Pointer // без указателей
func convT16(val uint16) unsafe.Pointer
func convT32(val uint32) unsafe.Pointer
func convT64(val uint64) unsafe.Pointer
func convTstring(val string) unsafe.Pointer
func convTslice(val []byte) unsafe.Pointer

Идея: для частых типов runtime упаковывает значение быстрее, чем общий convT.

Boxing сценарии

go
// 1. Указатель — без аллокации, data = сам указатель
var w io.Writer = &buf  // data = &buf

// 2. Маленькие значения — оптимизация "direct iface"
// Если значение помещается в указатель и не содержит указателей:
var x any = int64(42)  // может быть без аллокации

// 3. Нулевые значения — статический zeroVal
var x any = 0          // data указывает на runtime.zeroVal
var y any = ""         // data указывает на runtime.zeroVal
var z any = false      // data указывает на runtime.zeroVal

// 4. Большие или со ссылками — аллокация на heap
var s any = MyStruct{} // копия на heap

Практически: boxing копирует значение и может аллоцировать в циклах.

convT64 пример

go
// runtime/iface.go
func convT64(val uint64) unsafe.Pointer {
    // Оптимизация: статический пул для маленьких значений
    if val < uint64(len(staticuint64s)) {
        return unsafe.Pointer(&staticuint64s[val])
    }
    // Иначе аллокация
    p := mallocgc(8, uint64Type, false)
    *(*uint64)(p) = val
    return p
}

// Статический пул для значений 0-255
var staticuint64s = [256]uint64{0, 1, 2, 3, ..., 255}

Смысл: маленькие числовые значения можно хранить в статическом массиве без аллокации.

Direct Interface Optimization

Для типов, значение которых помещается в указатель:

go
// Компилятор проверяет:
// 1. Size <= PtrSize (8 байт на 64-bit)
// 2. Тип не содержит указателей (чтобы GC не сканировал)

// Direct iface возможен для:
// - int8, int16, int32, int64
// - uint8, uint16, uint32, uint64
// - float32, float64
// - bool
// - маленькие struct без указателей: struct{a, b int32}

type tinyStruct struct {
    a int32
    b int32
}

var x any = tinyStruct{1, 2}
// x.data содержит само значение (packed в 8 байт)
// НЕТ аллокации на heap!

Это работает только для pointer-free значений. Если есть указатели — нужна отдельная аллокация, иначе GC не увидит ссылки.

Method Dispatch

Вызов метода через интерфейс — это indirect call через itab.fun[]:

go
var w io.Writer = &buf
w.Write(data)  // Как это работает?

Шаги dispatch:

  1. Берём w.tab (itab)
  2. Достаём адрес нужного метода из itab.Fun[i]
  3. Передаём w.data как receiver
  4. Делаем indirect call

Скомпилированный код (псевдо-asm)

asm
; w.Write(data)

; 1. Загрузить itab из iface
MOVQ    w+0(SP), AX        ; AX = w.tab (*itab)

; 2. Проверка на nil (panic если nil)
TESTQ   AX, AX
JZ      panic_nil_interface

; 3. Загрузить адрес метода из itab.fun[0]
MOVQ    24(AX), CX         ; CX = itab.fun[0] = (*T).Write

; 4. Загрузить data (receiver)
MOVQ    w+8(SP), DX        ; DX = w.data

; 5. Вызвать метод
CALL    CX                 ; (*T).Write(DX, data)

Важно: если w = nil интерфейс, то tab будет nil и произойдёт panic ещё до вызова метода.

Стоимость dispatch

Direct call:     CALL target          ; 1 инструкция
Interface call:  MOVQ (iface), AX     ; загрузить itab
                 MOVQ 24(AX), CX      ; загрузить fun[i]
                 CALL CX              ; indirect call

Overhead:

  • 2 дополнительных load из памяти
  • Indirect call менее предсказуем для CPU branch predictor
  • Но современные CPU хорошо справляются с предсказанием indirect calls

На практике это означает: overhead заметен в tight loops, но в IO‑коде часто теряется на фоне системных вызовов.

Interface Memory Layout

iface
1/6
iface: пустой интерфейс с методами
var w io.Writer — tab и data оба nil. Размер 16 байт (2 указателя).
iface (16 bytes)
tabnil
datanil
Type info / itab
Data pointer
Method / Direct value

Interface Comparison

Интерфейсы можно сравнивать через ==:

go
var a, b io.Reader
a = &buf1
b = &buf2

// Сравнение интерфейсов
if a == b { ... }

Алгоритм сравнения

go
// runtime/iface.go
func ifaceeq(t *_type, x, y unsafe.Pointer) bool {
    if t == nil {
        // Оба nil
        return true
    }
    // Используем equal function из type descriptor
    return t.Equal(x, y)
}

Сравнение интерфейсов делает две вещи:

  1. Сравнивает динамические типы
  2. Если типы равны — сравнивает значения через функцию Equal у _type

Правила:

  1. Оба nil → равны
  2. Один nil, другой нет → не равны
  3. Разные dynamic types → не равны
  4. Одинаковые types → сравниваем значения через _type.Equal

Panic при сравнении

go
type MySlice []int

var a, b any
a = MySlice{1, 2}
b = MySlice{1, 2}

if a == b { } // 💥 PANIC: comparing uncomparable type MySlice

Slices, maps, functions — нельзя сравнивать, даже через интерфейс.

Причина: для таких типов нет операции ==, поэтому Equal не определён — runtime падает.

Производительность

Escape Analysis влияние

go
// Не escape — аллокация на стеке (быстро)
func process(r io.Reader) {
    // r используется только локально
    buf := make([]byte, 1024)
    r.Read(buf)
}

// Escape — аллокация на heap (медленнее)
func getReader() io.Reader {
    buf := &bytes.Buffer{}  // escape на heap
    return buf              // сохраняется в интерфейс
}

Почему это происходит: когда значение возвращается как интерфейс, компилятор часто вынужден переместить его в heap, чтобы интерфейс хранил стабильный адрес.

Devirtualization

Компилятор может убрать indirect call, если знает конкретный тип:

go
func process(w io.Writer) {
    w.Write(data)
}

// Инлайнинг + devirtualization:
func main() {
    var buf bytes.Buffer
    process(&buf)  // компилятор может инлайнить Write напрямую
}

Если компилятор видит конкретный тип в месте вызова, он может превратить w.Write в прямой вызов и убрать overhead интерфейса.

Benchmark: interface vs direct call

go
func BenchmarkDirectCall(b *testing.B) {
    buf := &bytes.Buffer{}
    data := []byte("hello")
    for i := 0; i < b.N; i++ {
        buf.Write(data)  // direct call
    }
}

func BenchmarkInterfaceCall(b *testing.B) {
    var w io.Writer = &bytes.Buffer{}
    data := []byte("hello")
    for i := 0; i < b.N; i++ {
        w.Write(data)  // interface call
    }
}

// Результаты (примерные):
// BenchmarkDirectCall-8       100000000    11.2 ns/op
// BenchmarkInterfaceCall-8    100000000    12.8 ns/op
// Разница в этом примере: ~15%
// Цифры зависят от CPU, версии Go и оптимизаций компилятора

Такие бенчмарки полезны как ориентир, но всегда измеряйте свои реальные hot paths.

Файлы runtime/

ФайлНазначение
runtime/iface.goОперации с интерфейсами: getitab, convT*, assertI2I
runtime/runtime2.goОпределения iface, eface, itab
internal/abi/iface.goITab структура (экспортируемая)
internal/abi/type.go_type и связанные структуры
cmd/compile/internal/ir/func.goГенерация method dispatch кода

Выводы

  1. iface vs eface — два разных представления для оптимизации пустого интерфейса

  2. itab — центральная структура для method dispatch, кешируется глобально

  3. Boxing имеет оптимизации для маленьких значений и статический пул для частых значений

  4. Method dispatch — indirect call через itab.fun[], overhead обычно небольшой, измеряйте в своём коде

  5. Escape analysis критична — присваивание интерфейсу часто вызывает escape на heap

Sources

Go Deep Dive — книга для Senior разработчиков