Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Middle ~18 мин 8 вопросов

Дженерики на собеседовании Go: type parameters, constraints, ловушки

Дженерики (Go 1.18+) вошли в стандартный набор собеседований: спрашивают синтаксис, ограничения и — главное — «когда дженерики НЕ нужны». Ниже вопросы с ответами на уровне «объясняю вслух». Сравнение с интерфейсами частично разобрано в теме про интерфейсы, практика — в продвинутом курсе.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

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=string

2. Что такое 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 — все типы, «базовым» типом которых является int

3. Что такое ~ (тильда) в ограничениях?

~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
}

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: