Интерфейсы и идентичность типов
Go обошёлся без классов: вместо наследования у него интерфейсы, которые тип удовлетворяет неявно, просто имея нужные методы. Из-за этой простоты механику интерфейса почти не учат — «ну реализует и реализует». Именно поэтому интерфейсы дают самые тихие и самые дорогие баги, те, что не падают при сборке и всплывают только на проде или на собеседовании.
Под простым синтаксисом прячется устройство. Интерфейс — не одно слово и не «ссылка на объект», а пара машинных слов: слово типа и слово данных. Ровно эта пара объясняет, почему интерфейс с nil-указателем внутри не равен nil, сколько стоит вызов метода через него и почему сравнение двух интерфейсов то компилируется, то паникует. Эта тема разбирает идентичность типов по слоям — от раскладки интерфейсного значения в памяти до правил сравнимости структур и ключей map.
Карта темы
- Представление интерфейса — интерфейсное значение как два слова, разница между
iface(с методами) иeface(пустойany). - Диспетчеризация интерфейса — косвенный вызов через
itab, его стоимость, отсутствие девиртуализации в общем случае и обобщения как родственный механизм. - Где объявлять интерфейс — интерфейс объявляют у потребителя; неявное удовлетворение даёт узкий нужный набор методов.
- Утверждение типа — формы
x.(T), comma-ok и type switch, их поведение при несовпадении динамического типа. - Typed nil — почему интерфейс, хранящий nil-указатель, не равен
nil, и как эта ловушка ломает обработку ошибок. - Вызов метода на nil-получателе — метод с указателем-получателем работает на
nil, пока не разыменует его. - Сравнимые типы — какие типы поддерживают
==и годятся в ключиmap; slice, map и func сравнимы только сnil. - Сравнение структур —
==сравнивает структуры поле за полем; равные дают один ключmap, а поле-slice ломает сравнимость.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать интерфейс одним словом или ссылкой на объект | Не объяснить ни typed nil, ни стоимость диспетчеризации |
Думать, что eface несёт itab | itab есть только у iface; eface хранит голый *_type |
| Считать вызов через интерфейс всегда девиртуализованным и бесплатным | Неверная модель стоимости; в общем случае это косвенный прыжок |
| Объявлять широкий интерфейс «про запас» у поставщика типа | Лишняя связанность; правильнее узкий интерфейс у потребителя |
Использовать форму x.(T) с одним результатом на недоверенном значении | Паника при несовпадении динамического типа вместо проверки ok |
Думать, что повторное x.(T) «закрепляет» тип за интерфейсом | Утверждение не меняет интерфейс — это лишь проверка |
Возвращать конкретный nil *T из функции с типом результата error | Интерфейс не равен nil — проверка if err != nil ложно срабатывает |
Считать, что вызов метода на nil-получателе всегда паникует | Он законен, пока тело не разыменует получатель |
Считать нулевую string равной nil | Нулевая строка — это ""; s == nil даже не компилируется |
Писать в nil-map | Чтение даёт нулевое значение, а запись паникует assignment to entry in nil map |
| Сравнивать два интерфейса с несравнимым динамическим типом | Компилируется, но паникует в рантайме comparing uncomparable type |
Класть поле-slice/map/func в структуру и сравнивать её через == | Несравнимость заразна — a == b не компилируется, ключом map тоже нельзя |
Значение для собеседований
Интерфейсы — основная часть Go-интервью, и спрашивают не синтаксис, а механику под ним: как интерфейсное значение лежит в памяти и что из этой раскладки следует.
Что обычно проверяют:
- Как интерфейсное значение лежит в памяти — два слова,
ifaceпротивeface. - Сколько стоит вызов метода через интерфейс и что такое
itab. - Где объявлять интерфейс и почему — у потребителя, узким набором методов.
- Как работают
x.(T), comma-ok и type switch и когда форма с одним результатом паникует. - Почему интерфейс с nil-указателем внутри не равен
nil— главная ловушка темы. - Почему метод на
nil-получателе то работает, то паникует. - Какие типы сравнимы, годятся в ключи
mapи почему поле-slice ломает сравнимость структуры.
Типичный неверный ответ: «интерфейс с nil внутри, конечно же, равен nil — это же nil». Это запускает разбор того, что интерфейс — два слова, и непустое слово типа делает значение non-nil, даже когда слово данных нулевое.