Generics в Go
Зачем это на интервью
Generics позволяют выразить один алгоритм или контейнер для семейства типов без any, reflection и дублирования. На интервью важнее не синтаксис, а выбор границы: кандидат E4 должен написать constraint и объяснить, почему generic helper лучше или хуже concrete функции и интерфейса. Избыточная параметризация делает API сложнее, не добавляя reuse.
Минимум для E4
- Объявить type parameter у функции и использовать его в
[]T,map[K]Vили результате. - Понимать constraint как интерфейс допустимых типов, а не runtime interface value.
- Использовать
comparableтолько там, где нужен==или ключ map. - Использовать
~в type set, если алгоритм должен принимать named types с тем же underlying type.
Компилятор часто выводит аргументы типов из обычных аргументов. У вызова Contains([]string{"go"}, "go") не нужно писать Contains[string]. Constraint comparable включает типы, разрешённые в операторах == и !=, и поэтому подходит для ключа map.
package main
import "fmt"
func Contains[T comparable](items []T, want T) bool {
for _, item := range items {
if item == want {
return true
}
}
return false
}
func main() {
fmt.Println(Contains([]string{"go", "rust"}, "go"))
}Для операций <, + и подобных constraint должен явно разрешать операторы. В стандартной библиотеке нет экспортируемого constraints.Ordered; определите локальный constraint либо используйте cmp.Ordered из пакета cmp в современных версиях Go.
package main
import "fmt"
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b {
return a
}
return b
}
func main() {
fmt.Println(Max(3, 5))
}Углубление для E5/Senior
Interface, использующийся как constraint, может содержать type terms (~int, A | B) и применим только как constraint; нельзя создать переменную такого interface type. Обычные методы в constraint задают операции, доступные для T; type set задаёт допустимое множество concrete types. Не проектируйте constraint «на будущее»: добавление type term или изменение union может сломать клиентов неочевиднее, чем изменение обычного интерфейса.
Generics не заменяют динамический полиморфизм. Если реализация выбирается во время работы (драйвер БД, отправитель уведомлений), используйте обычный interface. Если одна и та же статическая операция работает для многих форм данных (filter/map/min, typed cache), type parameter уместен. Для одной предметной сущности UserID concrete API обычно читается лучше generic контейнера с несколькими параметрами.
У generic type методы не могут иметь собственные type parameters: используйте parameters базового типа. Нельзя обращаться к полям T только потому, что все типы в union имеют такое поле; constraint обещает лишь разрешённые операции. Нет covariant/contravariant подтипирования []T: []Dog не является []Animal, даже если Dog реализует Animal.
package main
import "sync"
type Cache[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{m: make(map[K]V)}
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, ok := c.m[key]
return value, ok
}
func main() {
_ = NewCache[string, int]()
}Производительность измеряют, а не предполагают. Generics часто позволяют избежать boxing в interface{} и assertions, но generated code, inlining и escape behaviour зависят от версии компилятора и конкретного вызова. Для hot path проверяйте benchmark с -benchmem, а не выбирайте generic API только ради ожидаемого ускорения.
Ключевые понятия
| Понятие | Смысл | Пример |
|---|---|---|
| Type parameter | Переменная типа, известная при компиляции | T any |
| Type argument | Конкретный тип в instantiation | Cache[string, User] |
| Constraint | Ограничение допустимых типов и операций | K comparable |
| Type set | Множество типов constraint | `~int |
| Underlying type | Базовый тип named type | type UserID string имеет string |
any — предопределённый alias interface{}. Он полезен, когда алгоритму не нужны операции над T, например Clone[T any]; это не сигнал использовать any для неструктурированного JSON или dependency injection.
Типовые вопросы
- Когда выбрать generic, а когда interface?
- Generic — для статически типизированного алгоритма над несколькими типами; interface — для runtime-полиморфизма поведения.
- Зачем
~int, а неint?intдопускает только самint;~intтакже допускает named types с underlying typeint.
- Почему
T anyнельзя сравнить через==?anyдопускает несравнимые map, slice и func; нуженT comparableлибо другая операция сравнения.
- Можно ли вызвать поле
x.IDуT, ограниченного union structs?- Нет, полевая селекция не выводится из type set; проектируйте метод в constraint или передавайте accessor.
- Может ли метод generic типа добавить новый type parameter?
- Нет. Методы используют parameters receiver type; вынесите такую операцию в generic функцию.
- Делают ли generics код автоматически быстрее?
- Нет. Возможны преимущества над boxing, но решение подтверждают benchmark и профиль на конкретной версии Go.
Практика
- Напишите
Map[T, R any](in []T, f func(T) R) []RиFilter[T any].- Критерии готовности: порядок сохраняется; входной slice не меняется; тесты покрывают пустой input и nil slice; функция не использует
reflect.
- Критерии готовности: порядок сохраняется; входной slice не меняется; тесты покрывают пустой input и nil slice; функция не использует
- Реализуйте thread-safe
Cache[K comparable, V any]сGet,Set,Delete.- Критерии готовности: zero value либо явно документированно непригоден, либо безопасно работает; параллельный тест проходит
-race; нет наружной ссылки на internal map.
- Критерии готовности: zero value либо явно документированно непригоден, либо безопасно работает; параллельный тест проходит
- Замените две дублирующиеся функции поиска generic функцией.
- Критерии готовности: API остаётся читаемым; named type
UserID stringпроходит при нужном~string; добавлен benchmark до/после только при performance-цели.
- Критерии готовности: API остаётся читаемым; named type
Частые ошибки и ловушки
- Создавать generic abstraction до второго реального случая использования.
- Ограничивать
Tчерезany, а затем делать type assertions внутри алгоритма. - Заменять interface для runtime-сменяемой зависимости type parameter.
- Использовать
comparableкак «всё можно сравнивать содержательно»: pointer сравнивает адрес, struct — все сравнимые поля. - Ожидать covariance
[]Child→[]Parent.