Когда Go 1.18 представил Дженерики в 2022 году, он принес обобщённые параметры типов для функций и структур, но оставил методы. С выпуском Go 1.27 это долгосрочное ограничение было устранено. Методы теперь могут определять свои собственные параметры типов без добавления их в принимающую структуру.
Почему было введено это изменение? Если вы хотите создать узел графа, который содержит обобщённое значение, вы можете реализовать его так:
type Node[T any] struct {
value T
}
Представьте, что вы добавляете метод Map, который преобразует узел типа T в узел другого типа U. До Go 1.27 вы были вынуждены добавить U непосредственно в саму структуру Node:
type Node[T any, U any] struct {
value T
}
func (n *Node[T, U]) Map() Node[T, U] {
// ...
}
Добавление U в структуру само по себе является плохим дизайном, потому что U является параметром типа, специфичным для Map. Даже если другие методы не будут использовать U, им всё равно приходилось бы сохранять его в своих объявлениях получателя. Единственным обходным путем было реализовать Map как функцию уровня пакета, а не метод, поскольку функции могли определять свои собственные параметры типов. Но методы не могли, что приводило к неудобным и неидиоматичным проектам кода API. Начиная с Go 1.27, U может быть определён исключительно на методе Map:
func (n *Node[T]) Map[U any]() Node[U] {
// ...
}
Почему это заняло столько времени? Ответ кроется в том, как реализованы дженерики в Go. Компилятор обрабатывает дженерики в первую очередь с помощью мономорфизации, что означает, что он создаст копию обобщённых структур или функций для каждого конкретного типа, с которым они используются. Потому что в какой-то момент абстрактная концепция дженериков должна быть переведена в прямой машинный код. Однако система интерфейсов Go работает во время выполнения. Конкретный тип значения, переданного в параметр интерфейса, разрешается во время выполнения программы. Эта динамическая диспетчеризация сталкивается с дженериками, которые разрешаются во время компиляции. Подумайте, что произойдёт, если мы захотим объявить наш метод Map в интерфейсе:
type Mapper interface {
Map T any, U any U
}
Чтобы это заработало, система выполнения потребовала бы либо компилятор Just-In-Time для генерации машинного кода для конкретных типов на лету, либо создания этих копий для каждого возможного типа заранее, что привело бы к резкому увеличению размера бинарного файла. В конечном итоге, команда Go решила разделить обобщённые методы и интерфейсы. Go 1.27 позволяет использовать обобщённые методы на конкретных типах, но не на интерфейсах. Это различие объясняет, почему приведённый выше пример не компилируется и почему обсуждения по этому поводу заняли столько времени.
Источник: Hacker News · Сводку подготовил HeadlinesBriefing