Menu

Дженерики в Golang: параметры типа, ограничения и примеры

Дженерики (Go 1.18+) позволяют одной функции или типу работать со многими типами, сохраняя типобезопасность. Параметры типа, ограничения any, comparable и cmp.Ordered, свои ограничения с ~, обобщённые типы и когда дженерики не нужны.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Обобщённая функция

Параметры типа пишутся в квадратных скобках перед обычными параметрами. У каждого есть ограничение, которое говорит, какие типы допустимы.

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].

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ