Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Как читается синтаксис дженериков: type parameters, constraints, instantiation?
Обобщённая функция объявляет параметры типов в квадратных скобках: func Max[T cmp.Ordered](a, b T) T. Здесь T — параметр типа, cmp.Ordered — ограничение (constraint): набор допустимых типов. Использование Max(2, 3) — вывод типа сам подставит T=int (type inference); явная форма Max[int](2, 3) нужна редко. Обобщённые типы тоже есть: type Stack[T any] struct { items []T } — при использовании Stack[string] компилятор создаёт конкретный тип. Понимание терминов важно: путаница «constraint vs interface» — первый маркер поверхностного знания.
func Max[T cmp.Ordered](a, b T) T {
if a > b { return a }
return b
}
Max(2, 3) // T=int, выведен
Max("a", "b") // T=string2. Что такое constraint? Чем union-типы отличаются от интерфейсных ограничений?
Constraint — ограничение на аргумент типа. Бывает трёх видов: обычный интерфейс с методами (interface{ String() string }), union типов (int | int64 | float64 — «типсет», перечисление через |), и их комбинация. Union-типы дают компилятору знание о конкретных типах: внутри функции с T int | float64 можно писать a > b и a + b — операции разрешены для всех типов из набора. Интерфейсное ограничение методов таких операций не даёт: сравнить два Stringer нельзя. Термин для ответа: операторы разрешаются по union, а не по методам.
type Number interface { ~int | ~int64 | ~float64 }
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs { s += x } // += разрешён: union
return s
}
// ~int — все типы, «базовым» типом которых является int3. Что такое ~ (тильда) в ограничениях?
~T в constraint означает «любой тип, underlying type которого — T». Без тильды int допускает только буквально тип int; с ~int — и объявленные типы: type Age int подходит под ~int, но не подходит под int. Это критично для библиотечных ограничений: cmp.Ordered определён через ~int | ~int8 | … именно чтобы собственные типы вида type UserId int работали. Вопрос-проверка: напишите ограничение «любой целочисленный тип» и объясните разницу с/без тильды — чаще всего кандидаты её не знают.
4. Когда дженерики НЕ нужны?
Главный вопрос темы, и ответ на него ценнее синтаксиса. Не нужны: (1) когда достаточно интерфейса — поведение разное, код один и тот же (io.Reader закрывает 90% «хочу абстракцию»); (2) когда типы реально один — обобщать единственное использование это спекуляция; (3) когда ломаются семантики: map с generic-ключом требует comparable, и не всякий тип им бывает. Опасный паттерн — переписывание работающего кода на дженерики «для будущего»: усложнение вперёд без пользы. Формулировка-вывод: дженерики — инструмент для контейнеров и алгоритмов над группами типов (срезы, map, math), а не способ избежать интерфейсов.
5. Как дженерики влияют на производительность? Что такое GC shape stenciling?
Go не делает классический моно́морфинг (копию функции на каждый тип). Реализация — GC shape stenciling: одна скомпилированная версия на все типы с одинаковой «формой» (указатели — одна форма, все 8-байтные скаляры — другая), а различия типов передаются через словарь (dictionary), передаваемый в функцию. Следствие: вызов обобщённой функции может быть чуть медленнее конкретной (косвенность через словарь), но без раздувания бинарника, как в C++ шаблонах. Вопросы «а сколько стоит» часто решаются бенчмарком: в большинстве прикладных случаев разница в пределах шума.
6. Почему у обобщённых методов нет? (func (s Stack[T]) Push — можно, а метод с собственным параметром типов — нельзя)
Методы не могут объявлять собственные параметры типов: только тип-структура носит параметры, методы наследуют их. Причина — осложнения с интерфейсами: если бы методы могли добавлять параметры типов, набор методов типа переставал бы быть известным статически, и вся модель интерфейсов (itab) сломалась. Обходной путь — вынести операцию в обобщённую функцию верхнего уровня: func Map[T, U any](s Stack[T], f func(T) U) Stack[U]. На собеседовании ответ «комитет сознательно не взял эту сложность» + понимание itab — маркер глубины.
7. Что такое comparable и зачем он отдельным ограничением?
comparable — встроенное ограничение «тип поддерживает == и !=»: числа, строки, указатели, каналы, массивы сравнимых типов, структуры из сравнимых полей. НЕ входят: срезы, карты, функции. Нужно оно для типов, которые используют аргумент как ключ карты или сравнивают: func Group[T comparable](xs []T) map[T][]T — без comparable компилятор не разрешит m[x] = …. Ловушка: интерфейсные значения формально сравнимы, но паникуют при несравнимом динамическом типе — поэтому []any нельзя использовать как ключи безопасно, а компилятор это не остановит.
func Keys[T comparable, V any](m map[T]V) []T {
keys := make([]T, 0, len(m))
for k := range m { keys = append(keys, k) }
return keys
}8. Практическая задача: напишите обобщённую функцию Filter и объясните решения.
Ожидаемый ответ: сигнатура func Filter[T any](xs []T, pred func(T) bool) []T; предикат — аргумент-функция (не constraint-метод: предикатов бесконечно много, их нельзя перечислить union-ом); результат — новый срез с предварительным make(..., 0, len(xs)) по ёмкости; исходный срез не мутируется (чистая функция). Развитие вопроса: зачем возвращать новый срез вместо in-place — принцип наименьшего удивления и возможность цепочки Filter→Map; чем chains хуже обычных циклов — читаемость против слоёв абстракции. Здесь проверяют вкус: дженерики должны упрощать код, а не создавать мини-фреймворк.
func Filter[T any](xs []T, pred func(T) bool) []T {
out := make([]T, 0, len(xs))
for _, x := range xs {
if pred(x) { out = append(out, x) }
}
return out
}Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Язык Go с нуля: базовый синтаксис и конструкции — Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.