Обобщённая функция
Параметры типа пишутся в квадратных скобках перед обычными параметрами. У каждого есть ограничение, которое говорит, какие типы допустимы.
T и U выводятся из аргументов, поэтому писать Map[int, string](...) приходится редко. Вывод работает по аргументам функции; тип, который встречается только в результате, вывести нельзя. Для func Zero[T any]() T нужно писать Zero[int]().
До Go 1.18 такую функцию приходилось писать отдельно для каждого типа или принимать []interface{} и терять типобезопасность.
Ограничения
Ограничение это интерфейс. Оно сужает круг допустимых типов и взамен сообщает компилятору, какие операции разрешены над значениями этого типа.
| Ограничение | Допускает | Что можно делать |
|---|---|---|
any | любой тип | присваивать, передавать, хранить |
comparable | типы с поддержкой == | ==, !=, ключи мапы |
cmp.Ordered (Go 1.21) | целые, числа с плавающей точкой, строки | <, <=, >, >=, а также == |
| интерфейс с методами | типы с этими методами | вызывать методы |
объединение типов вроде ~int | ~float64 | ровно эти типы | операторы, которые поддерживают все они |
С any вместо comparable выражение v == target не скомпилируется: invalid operation: v == target (incomparable types in type set). Именно ограничение делает оператор допустимым.
Обе эти функции уже существуют: slices.Index и slices.Max. Стандартные пакеты slices, maps и cmp покрывают большинство повседневных обобщённых задач, так что сначала загляните в них.
Свои ограничения: объединения типов и ~
Ограничение может перечислять типы через |. Разрешены операторы, которые поддерживает каждый из перечисленных типов.
Тильда важна. ~int64 означает «любой тип, базовый тип которого int64», поэтому именованный тип Cents принимается. Если написать просто int64, Sum([]Cents{...}) упадёт с Cents does not satisfy Number (possibly missing ~ for int64 in Number).
Интерфейсы с объединениями типов можно использовать только как ограничения и никогда как обычные типы переменных. var n Number не компилируется.
В пакете golang.org/x/exp/constraints есть готовые ограничения Integer, Float и Signed. Он не входит в стандартную библиотеку, поэтому среда запуска на этой странице не может его импортировать; стандартное ограничение это cmp.Ordered.
Обобщённые типы
Структуры, слайсы, мапы и другие типы тоже могут иметь параметры типа. Методы обобщённого типа используют параметры получателя.
Две детали из этого примера встречаются постоянно:
- Нулевое значение T.
var zero Tэто способ вернуть «ничего» из обобщённого кода. Не существуетT{}илиnil, которые подходили бы для любого типа. - Инстанцирование. Обобщённый тип нужно инстанцировать перед использованием:
Stack[string], а неStack. Внутри его собственных методов получатель записывается какStack[T].
В Go 1.24 появились обобщённые псевдонимы типов: type Set[T comparable] = map[T]struct{} теперь компилируется.
Ограничения, на которые вы наткнётесь
- У методов нет параметров типа.
func (s *Stack[T]) Map[U any](...)не допускается. Используйте функцию верхнего уровня. - Нет специализации. Нельзя написать отдельную реализацию для
T = string. Обходной путь это type switch поany(v)внутри функции, и обычно это намёк, что дженерики здесь не тот инструмент. - Нет ограничений по имени оператора. Нельзя сказать «любой тип с методом
+»; типы перечисляются в объединении. - Доступ к полям через ограничение не работает. Даже если у всех типов объединения есть поле
ID,v.IDнедопустимо. Используйте в ограничении метод.
Когда дженерики не нужны
Дженерики подходят для контейнеров (стеки, множества, кеши, деревья) и алгоритмов над слайсами и мапами, которые делают одно и то же для любого типа элементов. Подходят они не везде.
- Один конкретный тип. Если функцию вызывают только с
[]User, обобщённая версия лишь усложняет чтение. - Поведение, различающееся по типу. Для этого есть интерфейсы. Функция, принимающая
io.Writer, проще, чем функция с[T io.Writer], и работает так же. - Замена параметров-интерфейсов.
func Print[T fmt.Stringer](v T)ничем не лучшеfunc Print(v fmt.Stringer).
Собственная рекомендация команды Go: сначала напишите код для конкретного типа, а к параметрам типа переходите, когда заметите, что пишете тот же код второй раз и меняются только типы.
Частые ошибки
anyтам, где нуженcomparableилиcmp.Ordered. Компилятор отвергает==или<для параметра типа с ограничениемany.- Забытая
~. Пользовательские типы вродеtype ID intне проходят ограничение, где написано простоint. - Возврат
nilдля параметра типа. Не допускается, если ограничение не сводит T к типам, похожим на указатели. Возвращайте нулевое значение. - Обобщённый помощник, который уже существует. Сначала проверьте
slices,mapsиcmp.
Часто задаваемые вопросы
Есть ли в Go дженерики?
Да, начиная с Go 1.18 (март 2022). Функции и типы могут объявлять параметры типа в квадратных скобках: func Map[T, U any](s []T, f func(T) U) []U. У каждого параметра типа есть ограничение: интерфейс, описывающий, что тип должен поддерживать.
Чем any отличается от comparable в дженериках Go?
any допускает любой тип, но со значениями почти ничего нельзя делать, кроме как присваивать, передавать и хранить. comparable допускает только типы, поддерживающие == и !=, а это нужно для ключей мапы или проверок на равенство. Для < и > используйте cmp.Ordered.
Что означает тильда (~) в ограничении Go?
~T означает «любой тип, базовый тип которого T». ~int совпадает с int, а также с type Celsius int. Без тильды int в ограничении совпадает только с самим int, и пользовательские типы будут отвергнуты.
Могут ли у методов в Go быть параметры типа?
Нет. Метод может использовать параметры типа своего получателя (func (s *Stack[T]) Push(v T)), но объявлять новые не может. Вместо этого напишите обобщённую функцию верхнего уровня: func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U].