Дженерики в 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-стек
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
n := len(s.items) - 1
v := s.items[n]
s.items = s.items[:n]
return v, true
}
Разбор:
[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 как основа веб-интеграций.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Основы языка Go - философия простоты, модель компиляции и идиоматичный подход к системной разработке. Go — это статически типизированный язык программирования общего назначения, разработанный компанией Google для создания эффективных, масштабируемых и надежных систем. Набор советов, правил, принципов и обычаев в разработке на этом языке. История Go - инженерные цели языка, философия простоты и эволюция инструментов экосистемы. Экосистема приложений на Go - встроенные инструменты, workflow разработки и практики сопровождения проектов. Кавычки, rune и string, точка, запятая, автоматическая вставка точки с запятой, скобки, подчёркивания и типичные ошибки новичков в Go. Предопределённые идентификаторы не являются ключевыми словами, но имеют специальное значение в языке. Их можно переопределить в локальной области видимости, но делать это не рекомендуется. Набор функций, которые включены в стандартную библиотеку языка. Особенности Go - интерфейсы, композиция, модель ошибок и практики написания поддерживаемого кода. Go вводит конкурентность через встроенные синтаксические конструкции и правила выполнения. Ниже рассматриваются основные направления практического применения Go, объяснённые через призму его технических характеристик и требований реальных инфраструктур. Типизация, набор правил определения типа данных значений языка.Основы языка Go
Что требуется знать перед началом изучения языка программирования Go
Рекомендации по разработке на Go
История языка Go
Экосистема приложений на Go
Синтаксис и пунктуация в Go
Ключевые слова языка Go
Встроенные функции и пакеты Go
Особенности языка Go
Синтаксические конструкции Go
Области применения Go
Типы данных и объявление переменных в Go