Дженерики появились в Go 1.18. Они позволяют писать функции и типы, параметризованные типами, без interface{} и приведения типов.
Функции с параметрами типов
func Map[T, U any](s []T, f func(T) U) []U {
res := make([]U, 0, len(s))
for _, v := range s {
res = append(res, f(v))
}
return res
}
lens := Map([]string{"a", "bb"}, func(s string) int { return len(s) }) // [1 2]
Типы обычно выводятся из аргументов. Явно: Map[string, int](...).
Ограничения
Ограничение это интерфейс. Помимо методов, в нём может быть набор типов:
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
~int означает «любой тип, у которого базовый тип int», например type Celsius int. Без тильды подойдёт только сам int.
Встроенные ограничения:
any: любой тип.comparable: можно==. Нужен для ключей мапы.cmp.Ordered(Go 1.21+): можно<. Числа и строки.
Интерфейс с набором типов можно использовать только как ограничение, не как тип переменной.
Обобщённые типы
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 }
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}
Нулевое значение параметра типа получают через var zero T.
Ограничения языка
- У методов не может быть своих параметров типов: только у типа или у функции.
- Нельзя сделать type switch прямо по
T, только по значению, приведённому кany. - Специализаций и перегрузки нет.
Стандартная библиотека
Пакеты slices и maps (Go 1.21) дают обобщённые slices.Sort, slices.Contains, slices.Index, maps.Keys, cmp.Compare, функции min и max встроены в язык. С Go 1.23 появились итераторы iter.Seq[V] и range по функциям, а maps.Keys возвращает итератор. В Go 1.24 разрешены обобщённые псевдонимы типов.
Как это компилируется
Go использует смешанный подход GC shape stenciling: одна копия кода на «форму» типа (все указатели имеют одну форму) плюс словарь с информацией о типах. Поэтому дженерик через указатели может работать чуть медленнее, чем код под конкретный тип, но быстрее, чем через interface{} с приведением.
Когда не нужны
Если функции нужно только вызвать методы, достаточно обычного интерфейса: func Print(s fmt.Stringer). Дженерики хороши для контейнеров и алгоритмов над слайсами и мапами.
Что спросят на собесе
- Что такое ограничение типа, зачем
~. - Чем
comparableотличается отany, аanyотinterface{}(ничем, псевдоним). - Почему у метода не может быть параметров типа.
- Когда дженерик лучше интерфейса и наоборот.
- Как реализованы дженерики: мономорфизация или словари.