Интерфейс — это пара «какой тип внутри + само значение». Нильный указатель в интерфейсе не равен 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, если тип не гарантирован.