Дженерики позволяют одному коду работать с разными типами, а компилятору по-прежнему их проверять: Stack[int] принимает только int, не требует утверждений типа и хранит значения по месту, а []any принимает что угодно и ничего не проверяет до рантайма.
Типобезопасность: с []any неверная запись в контейнер проявится паникой v.(int) в проде; со Stack[int] это ошибка компиляции.
Нет упаковки: неуказательное значение, лежащее в any, обычно копируется в кучу (исключение составляют малые целые и нулевые значения); []int хранит значения по месту.
Сигнатуры говорят сами за себя: func Keys[K comparable, V any](m map[K]V) []K ясно показывает, что на входе и что на выходе; func Keys(m any) []any этого не показывает.
До Go 1.18 были только interface{}, кодогенерация или копия кода под каждый тип. any по-прежнему уместен, когда значения действительно разнородны: разобранный JSON-документ, набор полей лога.
GoMiddle
Дженерики и параметры типов
Параметры типов, ограничения, type sets, вывод типов, GC shape stenciling
Закрыто вопросов
0 / 19
- 0Повторить
- 2Часто спрашивают
Вопросы
Типы-параметры пишутся в квадратных скобках перед обычными параметрами (func Map[T, U any](xs []T, f func(T) U) []U) и после имени типа (type Stack[T any] struct{ items []T }). Методы повторяют параметр в получателе: func (s *Stack[T]) Push(v T).
У каждого типа-параметра есть ограничение: any (что угодно), comparable (для ключей map и ==) или ваш собственный интерфейс.
При вызове типовые аргументы обычно выводятся: Map(ids, strconv.Itoa). Обобщённый тип нужно инстанцировать там, где он используется: var s Stack[int]; голый Stack не компилируется.
Метод использует T из получателя; собственные типы-параметры у метода разрешены только с Go 1.27, и такой метод не может реализовать интерфейс.
any ничего не обещает про T, поэтому в теле можно использовать только операции, которые поддерживает любой тип, а > к ним не относится: у срезов и структур нет порядка. Ограничьте T через cmp.Ordered, и > станет допустим.
**comparable**: добавляет == и !=; этого хватает для ключей map, Index и Contains.
**cmp.Ordered** (Go 1.21): все целые, вещественные и строковые типы, каждый с ~; даёт <, >, а заодно и +.
Объединение вроде ~int | ~float64 даёт операторы, общие для всех его членов: + - * / для Sum.
Для двух обычных значений свой Max вообще не нужен: встроенный max(a, b) есть с Go 1.21, а для среза есть slices.Max.
Объявите **var zero T** и верните её: у типа-параметра нет литерала нулевого значения, ведь nil, 0 и "" подходят только некоторым типам.
nil служит нулевым значением только для указателей, срезов, map, каналов, функций и интерфейсов; 0 только для чисел; "" только для строк. А T any может оказаться любым из них.
T{} требует, чтобы у всех типов из ограничения T был один и тот же базовый тип (структура, массив, срез или map); у any это не так.
Ноль является настоящим значением, а не его отсутствием, поэтому поиск возвращает (T, bool): найденный 0 и отсутствующий int дают одни и те же биты.
*new(T) представляет собой старую однострочную форму того же самого; var zero T читается лучше.
~ означает «базовый тип»: **~string** допускает любой именованный тип, построенный на string, а голый string допускает только сам string.
Ограничение задаёт множество типов. В interface{ string } ровно один тип, и type UserID string в него не входит.
~string включает string и любой type X string из любого пакета, а операции над T сохраняют именованный тип.
Тильда ставится только перед типом, который сам себе базовый, и никогда перед интерфейсом.
cmp.Ordered записан как ~int | ... | ~string именно поэтому, и поэтому slices.Sort принимает []UserID.
Вывод опирается на типы переданных аргументов, но никогда на тип, которому присваивается результат. Тип-параметр, встречающийся только в результате, приходится писать явно.
В Map[S, T](xs []S, f func(S) T) срез даёт S, а функция даёт T, поэтому в Map(users, nameOf) писать нечего.
var s *Set[int] = NewSet() всё равно падает с cannot infer T: левая часть присваивания не участвует в выводе.
Частичное инстанцирование разрешено: Map[User](users, f) задаёт первый параметр, остальные выводятся.
С Go 1.21 обобщённая функция, используемая как значение, выводится из цели: var f func(int) string = Format работает без Format[int].
Обычно это конструкторы, а ещё аргумент без собственного типа, например нетипизированный nil.
Интерфейс, который перечисляет типы (объединение или любой элемент с ~), служит только ограничением, и типом значения он быть не может. Готовый Ordered с Go 1.21 лежит в стандартной библиотеке как cmp.Ordered; старый импорт golang.org/x/exp/constraints больше не нужен.
Для «int или float64» нечего положить в слово типа у значения, поэтому var n Number отвергается с interface contains type constraints. У обычных интерфейсов с методами такое представление есть.
Если нужно значение, берите any с type switch. Ограничение работает как фильтр времени компиляции, и ничего больше.
Встраивание даёт пересечение: interface{ cmp.Ordered; fmt.Stringer } означает упорядоченный и печатаемый. Объединение встроенных ограничений объединяет их множества типов.
comparable можно встраивать, но нельзя делать элементом объединения.
У типа-параметра есть только те методы, которые объявляет его ограничение, а объединение объявляет типы, а не методы: общий для обоих членов String невидим. Встройте fmt.Stringer рядом с объединением.
Элементом объединения может быть неинтерфейсный тип, при желании с ~, либо интерфейс без методов: ~string | fmt.Stringer отвергается.
Зато объединение даёт операторы, которые поддерживают все его члены: + и < для ~int | ~string, len и индексирование для ~[]byte | ~string.
Объединение и fmt.Stringer в одном интерфейсе дают пересечение, и тогда v.String() компилируется.
Если нужно просто «что угодно с String()», это обычный параметр fmt.Stringer, и тип-параметр ничего не даёт. Объединение берут ради операторов.
Выведет int other stringer. Type switch сопоставляет точный динамический тип, поэтому MyInt не считается int, хотя базовый тип у него int; time.Duration попадает в fmt.Stringer, потому что у него есть метод String.
switch v.(type) над значением типа-параметра не компилируется (cannot use type switch on type parameter value v): type switch требует операнд интерфейсного типа, а T является типом-параметром, а не интерфейсом. Выход в том, чтобы сначала преобразовать в any, и это может упаковать v в кучу.
В type switch нет ~: case ~int не существует. Чтобы охватить именованные типы, их перечисляют по одному или переключаются по reflect.TypeOf(v).Kind().
Switch выполняет проверку в рантайме на каждом вызове, а не выбор на этапе компиляции для каждой инстанциации.
Type switch по T обычно означает, что поведение различается от типа к типу, а для этого и есть метод интерфейса.
any(v) == nil истинно только для nil-интерфейса. Нулевой *Config, приведённый к any, превращается в ненулевой интерфейс с типизированным nil внутри, поэтому OrDefault возвращает cfg, и c.Timeout разыменовывает nil-указатель.
Почему: значение интерфейса состоит из пары (динамический тип, значение), и оно равно nil, только когда пусты обе части. После any(v) в слоте типа лежит *Config, так что проверка может сработать, только если T сам интерфейсный тип, например error.
Почему баг проскакивает: тест с OrDefault[error](nil, def) проходит; баг проявляется только с указателем в качестве типового аргумента.
Исправление: ограничьте T через comparable и сравнивайте с var zero T: для указателя нулевое значение равно nil, а для интерфейса это nil-интерфейс. Стандартный cmp.Or(cfg, def) (Go 1.22) делает ровно это.
Компромисс: проверка на нулевое значение считает «не заданными» и 0, и "", а comparable не пропускает срезы и map. Если нужен «только nil-указатель», принимайте указатели явно: func OrDefault[T any](v, def *T) *T.
Как поймать: тестируйте обобщённые хелперы с указателем, интерфейсом и обычным значением в качестве типового аргумента; any(v) == nil в обобщённом коде даёт повод присмотреться на ревью.
comparable обещает лишь то, что == компилируется. С Go 1.20 обычный интерфейс вроде any удовлетворяет этому ограничению, а для интерфейсных значений настоящая проверка идёт в рантайме.
Сравнение двух интерфейсных значений сначала сравнивает их динамические типы. Если этот тип оказывается срезом, map или функцией, рантайм паникует: comparing uncomparable type []int.
Тоже паникует map[any]int со срезом в ключе: hash of unhashable type []int. Дженерики не добавили опасность, они перестали её прятать.
Чтобы это снова стало невозможным, перечислите поддерживаемые типы: ~string | ~int64. Или принимайте сравнение от вызывающего, как slices.IndexFunc.
Структура с полем any несёт ту же ловушку: она сравнима и всё равно может паниковать.
При промахе Load возвращает nil any, и однозначное утверждение типа v.(V) паникует с interface conversion: interface {} is nil, not main.User раньше, чем вызывающий код увидит ok == false.
Почему: утверждение типа без второго значения паникует всякий раз, когда в интерфейсе нет V, а в nil-интерфейсе нет ничего. Дженерики дают Get типизированную сигнатуру, но sync.Map внутри по-прежнему хранит any, так что утверждение остаётся проверкой в рантайме.
Исправление: форма comma-ok val, _ := v.(V) никогда не паникует: при промахе val получает нулевое значение V, а ok из Load остаётся false.
**Интерфейсный V**: у Cache[string, error] вызов Put(k, nil) тоже сохраняет nil-интерфейс. Форма comma-ok вернёт для него (nil, true), а исправление, которое лишь выходит раньше при !ok, там всё равно запаникует.
Как поймать: тест на ветку промаха и линтер forcetypeassert из golangci-lint, который помечает каждое непроверенное утверждение типа.
Она никогда не возвращает 0, поэтому каждая из двух строк с одинаковым счётом утверждает, что она больше другой. slices.SortFunc нужен согласованный трёхзначный результат: отрицательный, ноль, положительный.
slices.SortFunc не стабильна, поэтому равные строки выходят в произвольном порядке, если функция не разрешает ничью.
Возвращайте cmp.Compare(a.Score, b.Score) (Go 1.21): он даёт -1, 0 или +1, а для вещественных чисел ставит NaN перед всеми остальными значениями, вместо того чтобы ломать сортировку.
Разрешайте ничьи явно: cmp.Or(cmp.Compare(...), strings.Compare(a.Name, b.Name)); cmp.Or возвращает первый ненулевой аргумент.
Или сохраняйте исходный порядок равных через slices.SortStableFunc.
Обычно ошибка приходит из переноса с sort.Slice, где колбэком служит less, возвращающий bool.
iter.Seq[V] определён как func(yield func(V) bool): вы вызываете yield для каждого значения и останавливаетесь, как только он вернул false; это значит, что тело цикла сделало break или return. Очистка идёт в defer внутри итератора, потому что только итератор знает, что он открыл.
Каждая точка выдачи выглядит как if !yield(v) { return }. Если проигнорировать false, будет паника в рантайме: range function continued iteration after function for loop body returned false.
Один defer f.Close() наверху покрывает и исчерпание, и ранний break, и ошибку; цикл for у вызывающего не может закрыть файл, которого не открывал.
Адаптер вроде Filter обязан пробросить false внешнего yield внутрь, иначе источник продолжит работать после ухода потребителя.
iter.Pull превращает последовательность в функции next/stop, чтобы идти по двум сразу; если вызывающий не дочитывает её до конца, он обязан вызвать stop, иначе отложенная очистка итератора не выполнится.
maps.All, maps.Keys и slices.Values возвращают такие итераторы.
Тип-параметр нужен, когда код одинаков для любого типа и меняется только тип элемента: контейнеры, алгоритмы в духе slices, операторы вроде +. Когда поведение различается от типа к типу, берите интерфейс; когда типов всего два-три, конкретный код часто понятнее.
Параметр, который только протаскивают насквозь: Repo[T, ID], у которого все различия вынесены в шесть полей-функций, ничего не обобщает; настоящей абстракцией служат эти замыкания, а три конкретных репозитория лучше читаются и профилируются.
**Type switch по T**: это any с лишним синтаксисом; поведение должно жить в методе интерфейса.
Единственная точка вызова: конкретная функция, которую усложнили для чтения и для grep.
Параметр-интерфейс уже работает: func Save(w io.Writer) не нуждается в [W io.Writer], если только не нужно вернуть вызывающему его конкретный тип.
Что оправдывает тип-параметр: алгоритм, в логике которого тип элемента не участвует (slices.Chunk, очередь задач), или оператор, который не выразить интерфейсом: Sum нужен +.
Параметризуйте и сам срез (func Scale[S ~[]E, E ~int32](s S, c E) S), и типом результата становится то, что передал вызывающий. E по-прежнему выводится из S, поэтому точка вызова не меняется.
При E = int32 старый тип результата буквально равен []int32. Point проходит, потому что присваиваем к []int32, но это не тот же тип, а Norm объявлен на Point.
Когда S является параметром, make(S, len(s)) строит Point, и наружу выходит Point.
Вывод сопоставляет S с Point по аргументу, а затем достаёт E из ограничения ~[]E, приложив []E к базовому типу Point, то есть []int32.
Поэтому в стандартной библиотеке пишут func Sort[S ~[]E, E cmp.Ordered](x S): S существует ради именованных типов вызывающего, а не ради алгоритма.
Через методы типы реализуют интерфейсы, а обобщённому методу понадобилось бы тело для каждого U, при том что интерфейсы проверяются в рантайме. Go 1.27 разрешает типы-параметры у методов конкретных типов, но сохраняет запрет для методов интерфейсов: обобщённый метод никогда не реализует интерфейс.
Приведение типа вроде x.(interface{ Map(func(int) string) *Set[string] }) могло бы потребовать инстанциацию, которую не создал ни один код в сборке; её таблицу методов пришлось бы генерировать в рантайме.
С параметрами самого получателя дело обстоит иначе: Set[int] представляет собой один конкретный тип с конечным множеством методов, поэтому func (s *Set[T]) Add(v T) компилируется.
Обходной путь даёт функция уровня пакета: func MapSet[T, U comparable](s *Set[T], f func(T) U) *Set[U]. Вы теряете цепочку вызовов и больше ничего.
Go 1.27 (август 2026) разрешает func (s *Set[T]) Map[U comparable](...); обобщённый метод не считается методом интерфейса с тем же именем, а методы интерфейсов по-прежнему не могут объявлять типы-параметры. На Go 1.26 и старше используйте функцию уровня пакета MapSet.
Возьмите два типа-параметра ([T any, PT interface{ *T; Validator }]): значение остаётся T, а методы берутся у *T, до которого внутри вы добираетесь через PT(&v).
Validate с указательным получателем принадлежит *Row, а не Row, и ограничение проверяется обычным правилом множества методов: Row does not satisfy Validator (method Validate has pointer receiver).
T = *Row лишь переносит проблему: var v T оказывается нулевым указателем, и функция теперь отдаёт указатель независимо от того, нужен он вызывающему или нет.
PT interface{ *T; Validator } задаёт пересечение: в множестве только *T, и у него должен быть Validate. PT(&v) выполняет бесплатное преобразование, приносящее множество методов.
Вызывающие по-прежнему пишут Decode[Row](b): PT выводится из T.
Go компилирует обобщённую функцию один раз на GC shape (грубо говоря, на раскладку памяти базового типа, причём у всех указательных типов она одна) и передаёт каждой инстанциации скрытый словарь с информацией о типах. Это не шаблоны C++ и не интерфейсы.
Что генерирует компилятор
First[*A] и First[*B] делят одно тело, First[go.shape.*uint8]; у Sum[int] и Sum[float64] тела свои.
Словари (main..dict.First[*main.A]) несут то, что общему телу нужно в рантайме: настоящий тип для make([]T, n), для преобразования T в any, для поиска методов.
Бинарник растёт с числом раскладок, а не инстанциаций, и значения лежат по месту, без упаковки каждого элемента.
Когда это медленнее
Вызов метода на указательном T сначала находит метод через словарь, а затем идёт косвенным вызовом. Он не инлайнится и не девиртуализируется, поэтому может проиграть обычному параметру-интерфейсу.
Если инстанцировать T интерфейсным типом, может быть ещё медленнее: поиск идёт через словарь, а затем через собственную таблицу методов интерфейса.
Быстрый случай дают значимые раскладки с операторами: Sum[int] компилируется в тот же цикл, что вы написали бы руками.
Чтобы проверить, go tool nm покажет раскладки, -gcflags=-m покажет решения об инлайнинге, а остальное решит бенчмарк.