Интерфейсы и nil

О лекции

Интерфейс — это пара «какой тип внутри + само значение». Нильный указатель в интерфейсе не равен nil. Отсюда ложные if err != nil и паники на методах.

Типы и память Go golang собеседование

Интерфейс описывает набор методов. Не иерархию классов. Тип подходит под интерфейс сам: достаточно методов с нужными сигнатурами. Так стыкуются io.Reader, io.Writer, error.

В бэкенде тот же приём режет зависимости. Хендлер знает UserStore, а не *pgxpool.Pool. Легче подменить хранилище в тесте.

Аналогия: бейдж и коробка. Бейдж говорит «какой это тип», коробка держит данные. Интерфейс пустой (равен nil), только если нет ни бейджа, ни коробки. Бейдж «это *PathError», а в коробке пусто — интерфейс уже не nil.

Пара тип–значение

Значение интерфейса — два слова: динамический тип и данные. Вызов метода идёт по динамическому типу. Пустой интерфейс any не требует методов и принимает всё. Это коробка без контракта, не «ООП».

Присваивание конкретного значения в интерфейс фиксирует динамический тип. Type assertion x.(T) без comma-ok паникует, если тип не тот. С ok — безопасная проверка. Type switch — тот же механизм для нескольких веток.

func returnsErr() error {
	var p *os.PathError = nil
	return p // тип *os.PathError, данные nil
	// caller: err != nil → true, хотя p == nil
}

func ok() error {
	var err error // оба слова nil
	return err    // err == nil
}

type Reader interface {
	Read([]byte) (int, error)
}

func use() {
	var r Reader = bytes.NewBufferString("hi")
	buf := make([]byte, 2)
	_, _ = r.Read(buf)
}

Интерфейс nil только если тип и значение оба nil. Типизированный nil ломает if err != nil.

Пример. Репозиторий делает var e *NotFoundError = nil и return e. Снаружи err != nil истинно. Хендлер думает, что ошибка есть, и отдаёт 500. Нужно return nil.

Удовлетворение и границы пакета

Не нужно писать implements. Компилятор проверит при присваивании в интерфейс или при передаче в параметр. Для тестов заводят маленький интерфейс вокруг реально нужных методов. Не тащат конкретный *sql.DB в домен. Интерфейс объявляют там, где его потребляют, а не «на всякий случай» в пакете реализации.

Метод с receiver по указателю виден у указателя. Value-методы видны и у значения, и у указателя. Из-за этого тип «вроде реализует интерфейс», пока не передадите T вместо *T.

Пример. У func (c *Client) Close() тип Client не удовлетворяет интерфейс с методом Close. Нужен *Client.

Nil внутри интерфейса

Интерфейс равен nil, только когда оба слова nil. Типизированный nil-указатель внутри ненулевого типа даёт интерфейс != nil, хотя внутри «пусто». На error это классика продакшена.

Вызов метода по интерфейсу с ненулевым типом и nil-данными заходит в метод с nil receiver. Это законно, если метод к этому готов. Часто не готов: разыменование и паника. Проверка if w == nil на параметре интерфейса не ловит типизированный nil.

Типичная ошибка

Вернуть var e *MyError = nil как error и дальше ветвиться по err != nil. Интерфейс не nil. Вторая: assertion без ok на недоверенном значении — паника. Третья: огромный интерфейс «на все методы репозитория» в чужом пакете.

На собеседовании

Интерфейс — тип плюс данные. Он равен nil, только если оба пустые. Типизированный nil ≠ nil интерфейса. Интерфейс — граница пакета, не наследование. Assertion — с comma-ok, если тип не гарантирован.