Интерфейсы в Go: внутреннее устройство, typed nil и ресиверы

Обновлено 2026-05-19

Что такое интерфейс под капотом

Интерфейс в Go — это самая обычная структура. Внутри она выглядит так:

type iface struct {
    // информация о конкретном типе
    // и о том, как он реализует интерфейс
    tab  *itab
    // указатель на само значение
    data unsafe.Pointer
}

Для пустого интерфейса interface{} (он же any) структура немного другая:

type eface struct {
    typ  *_type
    data unsafe.Pointer
}

Но смысл один: интерфейс хранит пару «тип + значение». Именно из-за этого возникает несколько неочевидных моментов, которые любят спрашивать на собеседовании.

Typed nil: почему err == nil бывает false

Один из самых частых вопросов на собеседовании:

type MyErr struct{}

func (*MyErr) Error() string {
    return "boom"
}

func run() error {
    var err *MyErr = nil
    return err
}

fmt.Println(run() == nil)

Что выведет? false.

Почему? Потому что error — это интерфейс, а внутри него уже лежит конкретный тип *MyErr. То есть значение nil, но тип есть:

type: *MyErr
data: nil

А интерфейс равен nil только тогда, когда у него нет ни типа, ни значения. Это и есть разница между nil interface и typed nil.

Сравнение интерфейсов: компилируется, но паникует

var x any = []int{1, 2, 3}

fmt.Println(x == x)

Код скомпилируется, но в runtime будет паника.

Почему? Сам интерфейс сравнивать можно, но внутри лежит слайс, а слайсы в Go не сравниваются. Компилятор видит any и пропускает код. А во время выполнения Go смотрит на реальный тип внутри и говорит: слайсы так сравнивать нельзя — и запускает панику. То есть any не отключает типизацию, а просто откладывает проверку на потом.

Method set: User{} или &User{}

type Store interface {
    Save() error
}

type User struct{}

func (u *User) Save() error {
    return nil
}

var _ Store = User{}  // ?
var _ Store = &User{} // ?

Первый вариант не скомпилируется, второй будет окей.

Почему? Потому что метод Save() объявлен на *User, а не на User. У *User этот метод есть, у User — нет. Поэтому логика интерфейсов чуть хитрее, чем просто «реализовать метод»: важно, на каком ресивере объявлен метод. Это и есть method set.

Ещё раз про typed nil

var s Sender

fmt.Println(s == nil)

Результат — true: интерфейс пустой, нет ни типа, ни значения. А теперь так:

type EmailSender struct{}

func (e *EmailSender) Send(text string) error {
    return nil
}

var sender *EmailSender = nil
var s Sender = sender

fmt.Println(s == nil)

Результат — false. Снова та же история: значение внутри nil, но тип *EmailSender уже лежит в интерфейсе.

Что нужно уверенно понимать про интерфейсы

Похожие статьи