Дженерики в Go
С Go 1.18 (2022) в языке появились дженерики (обобщённые типы и функции). Компилятор подставляет конкретные типы на этапе сборки (мономорфизация), без рефлексии в рантайме. Статья дополняет особенности языка и рефлексию.
Теория: Обобщения и обобщённое программирование.
См. также: Важные интерфейсы · Синтаксические конструкции · Сравнение GC в Java, Python и Go.
Зачем дженерики, если есть интерфейсы
До Go 1.18 повторяющийся код для []int, []string, map[K]V копировали или писали через interface{} и type assertion. Интерфейсы по-прежнему основа полиморфизма в Go, но они описывают поведение, а не "любой срез с элементом, у которого есть поле ID".
| Подход | Когда уместен |
|---|---|
| Интерфейс | Разные типы с общими методами (io.Reader, sort.Interface) |
| Дженерик | Один алгоритм для разных типов данных (Min, Contains, Map) |
| Рефлексия | Тип заранее неизвестен, нужен универсальный декодер (дорого и хрупко) |
go generate | Шаблоны до 1.18; сейчас чаще заменяют generics |
Дженерики добавили выразительность без "шаблонного ада". Если задачу решает интерфейс из трёх методов — интерфейс предпочтительнее.
Generics — для библиотечных алгоритмов и контейнеров, а не для всей доменной модели.
Синтаксис — type parameters
Обобщённая функция
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// вызов: компилятор выводит T
x := Min(3, 7) // int
y := Min(1.5, 2.0) // float64
Разбор:
[T cmp.Ordered]объявляет параметр типаTи ограничение (constraint)cmp.Orderedиз пакетаcmp(Go 1.21+).- Ограничение говорит — для
Tдопустимы операции<,>,==— иначе сравнение вMinне скомпилируется. - При вызове
Min(3, 7)типintвыводится автоматически; явная форма:Min[int](3, 7).
Обобщённый тип
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
Разбор:
comparable— встроенное ограничение: ключи map и оператор==.Set[int]иSet[string]— разные конкретные типы; у каждого свой методAddс правильным типом аргумента.- Пустая структура
struct{}в map — идиома "множество без значения".
Ограничения (constraints)
Constraint — интерфейс, который может включать union типов и встроенные ограничения:
type Number interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, v := range xs {
total += v
}
return total
}
Разбор:
~intозначает "самintи любой тип с базовым типомint" (например,type UserID int).- Union
|перечисляет допустимые типы; вне спискаSumне применить. - Для произвольных типов с методами constraint оформляют как обычный интерфейс:
type Stringer interface {
String() string
}
func Join[T Stringer](items []T, sep string) string {
// ...
}
Частые ограничения
| Ограничение | Смысл |
|---|---|
any | Алиас interface{}; любой тип |
comparable | Можно сравнивать через == |
cmp.Ordered | Упорядочиваемые типы для < > |
| Собственный interface | Набор методов или union типов |
Стандартная библиотека — slices и maps
С Go 1.21 пакеты slices и maps дают generic-операции без своего кода:
import "slices"
xs := []int{3, 1, 4, 1}
slices.Sort(xs)
i := slices.Index(xs, 4)
cloned := slices.Clone(xs)
import "maps"
m1 := map[string]int{"a": 1}
m2 := maps.Clone(m1)
maps.DeleteFunc(m2, func(k string, v int) bool { return v == 0 })
Разбор:
slices.Sortработает для любого среза сcmp.Orderedэлементом.slices.Cloneкопирует срез с корректной ёмкостью — удобнее ручногоappend([]T(nil), xs...).maps.Clone/DeleteFunc— типобезопасные операции над map без рефлексии.
Дженерики и интерфейсы вместе
Интерфейс может быть constraint с методами; тип может быть одновременно generic и реализовывать интерфейсы:
type Identifiable interface {
ID() int64
}
func IDs[T Identifiable](items []T) []int64 {
out := make([]int64, len(items))
for i, it := range items {
out[i] = it.ID()
}
return out
}
Ограничение с методами заменяет ручные type switch по interface{}.
Осторожно: нельзя использовать type assertion к параметру типа без constraint с нужным методом; рефлексия в generic-коде редко нужна — см. рефлексию.
Когда не использовать дженерики
| Ситуация | Лучше |
|---|---|
| Один конкретный тип в проекте | Обычная функция без [T] |
| Полиморфизм по поведению | Интерфейс + DI |
| JSON/API с неизвестной схемой | json.Unmarshal в map[string]any или кодоген |
| "Универсальный" ORM на reflect | Проверенные библиотеки (GORM, ent), не свой generic-слой |
Три слабости рефлексии (медленно, без проверки на этапе компиляции, сложная отладка) частично снимаются generics, но не полностью — runtime-метаданные по-прежнему в reflect.
Практический пример — generic-стек
Код ITЗагрузка примера кода…
Разбор:
[T any]допускает любой тип элемента стека.zero T— нулевое значение типа параметра (дляintэто0, для указателя —nil).- В production чаще берут готовые структуры или срез; пример показывает механику type parameter.
Ключевые тезисы
- Дженерики в Go — compile-time мономорфизация, без рантайм-шаблонов C++.
- Constraint задаёт, какие операции и методы доступны для
T. slices/maps— первый выбор для сортировки, поиска, клонирования.- Интерфейсы остаются для поведения; generics — для алгоритмов над типами данных.
Мини-практикум
- Перепишите функцию
Containsдля[]intвContains[T comparable](xs []T, v T) bool. - Реализуйте
Map[T, U any](xs []T, f func(T) U) []Uи сравните с циклом без generics. - Замените ручную сортировку на
slices.SortFuncс кастомным компаратором.
Типичные ошибки
| Ошибка | Последствие |
|---|---|
interface{} там, где достаточно [T any] | Потеря типов, лишние assertion |
| Слишком широкий constraint | Сложные сообщения компилятора |
| Generic на каждую функцию | Шум в API, дольше компиляция |
Сравнение []byte через == в generic без comparable | Ошибка компиляции (срезы не comparable) |
Связанные материалы
- Особенности языка Go — история отказа от generics до 1.18
- Рефлексия в Go — когда тип известен только в runtime
- Важные интерфейсы и типы —
sort.Interface,io.Reader - Следующий шаг — gRPC в Go
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.