Перейти к основному содержимому

Дженерики в 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
Философия Go

Дженерики добавили выразительность без «шаблонного ада». Если задачу решает интерфейс из трёх методов — интерфейс предпочтительнее. 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 — для алгоритмов над типами данных.

Мини-практикум

  1. Перепишите функцию Contains для []int в Contains[T comparable](xs []T, v T) bool.
  2. Реализуйте Map[T, U any](xs []T, f func(T) U) []U и сравните с циклом без generics.
  3. Замените ручную сортировку на slices.SortFunc с кастомным компаратором.

Типичные ошибки

ОшибкаПоследствие
interface{} там, где достаточно [T any]Потеря типов, лишние assertion
Слишком широкий constraintСложные сообщения компилятора
Generic на каждую функциюШум в API, дольше компиляция
Сравнение []byte через == в generic без comparableОшибка компиляции (срезы не comparable)

Связанные материалы


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").