Система типов Go — где простота превращается в ловушку
Go намеренно сделал систему типов маленькой — горстка целых типов, интерфейсы вместо классов, никаких обобщений до версии 1.18. Из-за этой кажущейся простоты её почти не учат: «ну int и int, чего там разбираться». Именно поэтому она даёт самые тихие и самые дорогие баги — те, что не падают при сборке и всплывают только на проде или на собеседовании.
Под простым синтаксисом прячется механика. Целые типы — это фиксированные раскладки бит с конкретным поведением при переполнении, а ширина int зависит от платформы и фиксируется компилятором, а не выбирается в рантайме. Интерфейс — не «ссылка на объект», а пара машинных слов, и ровно эта пара объясняет, почему интерфейс с nil-указателем внутри не равен nil. Эта тема разбирает систему типов по слоям — от устройства одного целого до того, как компилятор инстанцирует обобщённый код.
Карта темы
- Знаковые и беззнаковые типы — дополнительный код, неотрицательные
uintи оборачивание по модулю 2^n при переполнении вместо ошибки. - Размеры целых типов — фиксированные ширины 8/16/32/64 бита и платформозависимый
int, ширина которого задаётся реализацией, а не рантаймом. - Диапазон целых типов — как дополнительный код делит биты между отрицательной и положительной частью и почему максимум
int64равен 2^63−1. - Представление интерфейса — интерфейсное значение как два слова, разница между
iface(с методами) иeface(пустойany). - Диспетчеризация интерфейса — косвенный вызов через
itab, его стоимость, отсутствие девиртуализации в общем случае и обобщения как родственный механизм. - Утверждение типа — формы
x.(T), comma-ok и type switch, их поведение при несовпадении динамического типа. - Typed nil — почему интерфейс, хранящий nil-указатель, не равен
nil, и как эта ловушка ломает обработку ошибок.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать, что uint при переполнении паникует или насыщается | Тихое оборачивание по модулю 2^n; бесконечный цикл или неверный счётчик |
Вычитать больший uint из меньшего, ожидая отрицательного результата | Получается огромное положительное число — нет промежуточного «минуса» |
| Думать, что знаковый тип хранит знак отдельным флаг-битом | Неверная модель дополнительного кода; ошибка в расчёте диапазона |
Считать int всегда 32-битным или всегда 64-битным | Несовпадение размеров между платформами; битый бинарный формат |
Полагаться на ширину int в сериализованных данных | Файл, записанный на 64-битной цели, не читается на 32-битной |
Брать максимум int64 как 2^64−1 или 2^63 | Это диапазон uint64 или ошибка на единицу; верно 2^63−1 |
| Считать интерфейс одним словом или ссылкой на объект | Не объяснить ни typed nil, ни стоимость диспетчеризации |
Думать, что eface несёт itab | itab есть только у iface; eface хранит голый *_type |
Возвращать конкретный nil *T из функции с типом результата error | Интерфейс не равен nil — проверка if err != nil ложно срабатывает |
Использовать форму x.(T) с одним результатом на недоверенном значении | Паника при несовпадении динамического типа вместо проверки ok |
| Считать вызов через интерфейс всегда девиртуализованным и бесплатным | Неверная модель стоимости; в общем случае это косвенный прыжок |
Значение для собеседований
Систему типов на Go-интервью спрашивают почти всегда — и не как определения, а как проверку того, видите ли вы механику под синтаксисом. Целые типы — тёплая часть разговора, интерфейсы — основная.
Что обычно проверяют:
- Разница знаковых и беззнаковых типов — дополнительный код и оборачивание по модулю 2^n.
- Какие ширины целых даёт Go и что определяет ширину
int— реализация и платформа, не рантайм. - Как вычислить диапазон
int64и почему его максимум — 2^63−1, а не 2^63 и не 2^64−1. - Как интерфейсное значение лежит в памяти — два слова,
ifaceпротивeface. - Сколько стоит вызов метода через интерфейс и что такое
itab. - Как работают
x.(T), comma-ok и type switch. - Почему интерфейс с nil-указателем внутри не равен
nil— главная ловушка темы.
Типичный неверный ответ: «интерфейс с nil внутри, конечно же, равен nil — это же nil». Это запускает разбор того, что интерфейс — два слова, и непустое слово типа делает значение non-nil, даже когда слово данных нулевое.