Дженерики пришли в Go в версии 1.18 — позже, чем в большинство мейнстрим-языков, и осознанно скромнее. Их часто продают как «типобезопасную замену interface{} без потери скорости», и первая половина этого тезиса верна. Вторая — только иногда. Реализация дженериков в Go — это не мономорфизация как в C++ (копия кода на каждый тип) и не стирание типов как в Java (один код, приведения в рантайме), а гибрид: GC-shape stenciling со словарями. У этого гибрида есть цена, и она проявляется ровно там, где её меньше всего ждут — на указательных типах.
Эта статья — про дженерики вглубь: синтаксис type parameters и constraints (включая ~-approximation и comparable), инференс типов и его границы, устройство под капотом и — главное — что это стоит, заземлённое на живой стенд с бенчмарками. Спойлер из стенда: на сумме []int дженерик и рукопись идут ноздря в ноздрю при нуле аллокаций, а interface{}-версия дороже в десятки раз и боксит тысячи раз; но на вызове метода через constraint по указательному типу дженерик не быстрее обычного интерфейса. Про то, почему так, и весь разговор.
Языко-агностичный взгляд на ту же тему — в компаньоне «Дженерики across languages»: там сравнение подходов C++/Java/Rust/Go бок о бок. Здесь — только Go и только вглубь.
В статье
- Зачем дженерики и когда они не нужны
- Type parameters и constraints
- Инференс типов
- Под капотом: GC-shape stenciling и словари
- Цена: бенчмарки на живом стенде
- Когда дженерики выигрывают, когда нет
- Границы механизма
- Демо и версии
- Документация и первоисточники
Зачем дженерики и когда они не нужны
До 1.18 в Go было два способа написать код, работающий с разными типами, и оба неприятны. Первый — interface{} (с 1.18 — синоним any): универсально, но типобезопасность теряется на входе и восстанавливается приведением на выходе, а компилятор ничего не проверяет. Второй — кодогенерация (go generate, text/template): типобезопасно и быстро, но требует внешнего шага сборки, засоряет репозиторий и плохо переживает рефакторинг.
Дженерики закрывают именно эту нишу: параметрический полиморфизм с проверкой на этапе компиляции и без кодогена. Каноничный пример — контейнер, который раньше пришлось бы либо копипастить, либо оборачивать в any:
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
}Stack[int] и Stack[string] — разные типы, каждый проверен компилятором; Pop возвращает T, а не any, приведений на стороне вызова нет. Обратите внимание на var zero T — это идиома «нулевое значение параметра типа», единственный способ получить нейтральный элемент, не зная T.
Но дженерики — не серебряная пуля, и авторы Go это прямо декларируют. Правило из официального блога: если реализации метода для разных типов различаются — это интерфейс, а не дженерик. Дженерики уместны, когда код над разными типами буквально одинаков (алгоритмы над срезами/картами, контейнеры, утилиты). Если же для каждого типа своя логика — нужен полиморфизм через интерфейсы, а не параметр типа. И почти всегда: не начинайте с дженерика — сначала конкретный тип, обобщение потом, когда дублирование стало реальным, а не гипотетическим.
Type parameters и constraints
Синтаксис параметра типа — квадратные скобки после имени: func F[T Constraint](...), type C[T Constraint] struct{...}. T — имя параметра, Constraint — ограничение, задающее, что с T можно делать. В отличие от any в старом коде, constraint — это контракт, проверяемый компилятором.
Constraint в Go — это интерфейс, но расширенный. Классический интерфейс ограничивает по методам; constraint дополнительно может ограничивать по множеству допустимых типов. Оба механизма комбинируются в одном объявлении.
Constraint по методам
Если внутри дженерика вызывается метод — constraint перечисляет этот метод, ровно как обычный интерфейс:
type Stringer interface {
String() string
}
func Join[T Stringer](xs []T) string {
var b strings.Builder
for i, x := range xs {
if i > 0 {
b.WriteString(", ")
}
b.WriteString(x.String()) // x.String() разрешён: T ограничен Stringer
}
return b.String()
}Отдельный, но частый на практике вид — self-referential (F-bounded) констрейнт, где интерфейс ссылается на сам тип-параметр. Так описывают «тип, который умеет сравнивать себя с себе подобным»: метод принимает T, а не any, и компилятор проверяет это статически.
// Ordered[T]: тип T умеет сравнивать себя с другим T.
type Ordered[T any] interface {
Less(T) bool
}
// Min работает с любым типом, реализующим Less(себя): T ограничен Ordered[T].
func Min[T Ordered[T]](a, b T) T {
if b.Less(a) {
return b
}
return a
}T Ordered[T] читается «T таков, что реализует Ordered[T]» — рекурсия в объявлении легальна и типобезопасна: Less принимает именно T, а не «какой-нибудь Ordered». Это идиома для обобщённых компараторов, fluent-builder’ов (WithX(...) T, возвращающих свой конкретный тип) и подобных API, где метод должен оперировать точным типом-получателем, а не общим интерфейсом.
Constraint по множеству типов и ~-approximation
Более интересная часть — ограничение по типам. Внутри интерфейса-constraint можно перечислить конкретные типы через union |. Тогда T обязан быть одним из них, и компилятор разрешает операции, определённые для всех членов union (например, +, <):
type Ordered interface {
~int | ~int64 | ~float64 | ~string
}
func Max[T Ordered](a, b T) T {
if a > b { // оператор > определён для всех членов union
return a
}
return b
}Ключевая деталь — тильда. int в union означает «ровно тип int». ~int означает «int и любой тип, чей базовый тип (underlying type) — int». Разница критична, потому что в Go повсеместно объявляют именованные типы поверх встроенных:
type Celsius float64 // базовый тип — float64
// Constraint без тильды: только сам float64.
type StrictFloat interface{ float64 }
// Constraint с тильдой: float64 и всё, что на нём построено.
type AnyFloat interface{ ~float64 }
func f1[T StrictFloat](x T) {}
func f2[T AnyFloat](x T) {}
func demo() {
var c Celsius = 36.6
// f1(c) // НЕ компилируется: Celsius не является буквально float64
f2(c) // OK: базовый тип Celsius — float64, подходит под ~float64
}Практический вывод: в constraint по числовым/строковым типам почти всегда нужна тильда — иначе ограничение отсечёт все пользовательские типы поверх встроенных. Именно поэтому constraint Number в стенде записан как ~int | ~int8 | ... | ~float64 — чтобы type Money int64 тоже проходил.
comparable
Отдельный встроенный constraint — comparable. Он разрешает операции == и != и нужен, например, чтобы использовать T как ключ карты:
func Unique[T comparable](xs []T) []T {
seen := make(map[T]struct{}, len(xs))
out := xs[:0]
for _, x := range xs {
if _, ok := seen[x]; !ok {
seen[x] = struct{}{}
out = append(out, x)
}
}
return out
}Тонкость, о которой спотыкаются: comparable — это не «любой тип, поддерживающий ==». Интерфейсные значения формально сравнимы, но их сравнение может паниковать в рантайме (если под интерфейсом лежит несравнимый тип, вроде среза). Начиная с Go 1.20 правила смягчили: обычные интерфейсы стали удовлетворять comparable, но с оговоркой, что сравнение всё ещё может паниковать. Для строгой типобезопасности предпочитайте конкретные comparable-типы, а не интерфейсы под comparable.
Пакет constraints и переезд в slices/maps/cmp
На заре дженериков (1.18–1.20) базовые constraint’ы жили в экспериментальном golang.org/x/exp/constraints — оттуда брали constraints.Ordered, constraints.Integer и прочее. С Go 1.21 бо́льшая часть применений этих constraint’ов уехала в стандартную библиотеку: пакеты slices, maps и cmp дают готовые дженерик-функции, и напрямую писать Max/Min/Sort по Ordered больше не нужно.
import (
"cmp"
"slices"
)
func demo() {
xs := []int{3, 1, 2}
slices.Sort(xs) // сортировка по возрастанию
m := slices.Max(xs) // максимум
i, found := slices.BinarySearch(xs, 2)
_ = m
_ = i
_ = found
// cmp.Ordered — constraint из stdlib (аналог constraints.Ordered)
_ = clamp(5, 0, 10)
}
func clamp[T cmp.Ordered](v, lo, hi T) T {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}Ключевой сдвиг: cmp.Ordered из стандартного пакета cmp заменил constraints.Ordered как «тот самый» constraint для упорядочиваемых типов. Пакет x/exp/constraints всё ещё существует и полезен для более редких ограничений (Integer, Float, Signed), но повседневный код 1.21+ чаще опирается на cmp + slices + maps и вообще не объявляет собственных числовых constraint’ов.
Инференс типов
Хорошая новость: в большинстве случаев параметры типа писать руками не надо — компилятор выводит их из аргументов (type inference). Плохая — инференс не всесилен, и понимать его границы полезно.
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, len(xs))
for i, x := range xs {
out[i] = f(x)
}
return out
}
func demo() {
xs := []int{1, 2, 3}
// T выводится из xs ([]int → T=int),
// U выводится из возвращаемого типа f (int → U=string):
ss := Map(xs, func(x int) string { return strconv.Itoa(x) })
_ = ss
// Явные параметры типа — когда вывести не из чего:
empty := Map[int, string](nil, func(x int) string { return "" })
_ = empty
}Правило приблизительно такое: T выводится, если он встречается в типе хотя бы одного обычного (не-типового) аргумента функции. Map(xs, f) выводит T из xs, а U — из типа результата функции f (function argument type inference). А вот если параметр типа фигурирует только в возвращаемом значении и нигде в аргументах — вывести неоткуда:
func Zero[T any]() T {
var z T
return z
}
func demo() {
// z := Zero() // НЕ компилируется: из чего выводить T?
z := Zero[int]() // нужен явный параметр типа
_ = z
}Другие частые границы инференса:
- Константы без явного типа.
Max(3, 5)приMax[T Ordered]выведетT=int(default type нетипизированной константы), ноMax(3, 5.0)— ошибка:3хочет бытьint,5.0—float64, единогоTнет. Лечится приведением одного из аргументов или явнымMax[float64](3, 5.0). - Композитные литералы и
nil.nilне несёт типа — из него ничего не выведешь. - Инференс не проходит через несколько «слоёв» так глубоко, как в Hindley-Milner-языках. Go сознательно ограничивает вывод локальными правилами ради предсказуемости и скорости компиляции; при сомнении компилятор требует явности, а не гадает.
Практика: полагайтесь на инференс в вызовах, но не удивляйтесь, когда придётся дописать [T] — это не баг, а осознанный компромисс дизайна.
Под капотом: GC-shape stenciling и словари
Это ядро статьи. Вопрос, на который отвечает реализация: как один дженерик-код исполняется для разных T? У индустрии два классических ответа, и Go не выбрал ни один из них целиком.
- Мономорфизация (C++, Rust). Компилятор генерирует отдельную специализированную копию кода на каждый конкретный
T. Максимальная скорость (прямые вызовы, инлайн), но раздувание бинаря и времени компиляции — copy на каждый тип. - Стирание типов (Java). Один общий код, все параметры типа стёрты до
Object, типобезопасность — только на этапе компиляции, в рантайме — приведения и боксинг. Компактно, но с накладными расходами и без специализации.
Go выбрал середину — GC-shape stenciling со словарями. Идея в двух частях.
Часть 1 — стенсилинг по форме (shape), а не по типу. Компилятор генерирует копию кода не на каждый T, а на каждую GC-форму (GC shape) — грубо, на каждый уникальный layout памяти с точки зрения сборщика мусора и вызовов. Ключевой факт: все указательные типы имеют одну и ту же форму. *Circle, *Rect, *bytes.Buffer, *os.File — с точки зрения GC это всё «одно машинное слово, являющееся указателем». Значит, дженерик, инстанцированный по любому из них, использует одну общую копию кода. А вот int, float64, string, структуры разных layout’ов — это разные формы, у каждой своя копия.
Часть 2 — словарь (dictionary). Раз одна копия обслуживает много типов одной формы, ей нужно как-то узнавать про конкретный тип в рантайме: какой у него размер (для value-форм это уже зашито в форму, но для методов — нет), где лежат его методы, как его сравнивать/хешировать. Эту информацию компилятор упаковывает в скрытый параметр — словарь, который передаётся в дженерик-функцию неявно, как дополнительный аргумент. Словарь содержит указатели на нужные методы конкретного типа, метаданные типа (*runtime._type), под-словари для вложенных вызовов.
Отсюда — центральное следствие для производительности. Когда дженерик над указательным типом вызывает метод из constraint (x.Area()), этот вызов идёт не напрямую и не через инлайн, а косвенно, через словарь: код одной формы не знает статически, чей именно Area звать, — он берёт указатель на метод из словаря и прыгает по нему. Это ровно та же косвенность, что у обычного интерфейсного вызова через itab. Девиртуализации (превращения в прямой вызов) на этом пути нет. Именно поэтому, как мы увидим в бенчмарках, дженерик по указателям не быстрее интерфейса: под капотом это тот же самый indirect call, просто через словарь вместо itab.
Оговорка важная: всё это — как реализовано в текущем компиляторе Go (на момент go1.26.3) для описанных паттернов, а не гарантия языка. Спецификация не обещает ни shape-стенсилинга, ни словарей, ни отсутствия девиртуализации; это детали реализации gc, и они могут меняться от релиза к релизу (компилятор вправе девиртуализировать конкретный call-site, если докажет единственный тип). Поэтому механизм ниже описывает поведение, которое стоит перепроверять бенчем на своей сборке, а не константу, на которую можно закладываться навсегда.
Для value-форм картина другая и приятная: SumGeneric[int] — это специализация именно под форму int, тело цикла складывает машинные слова напрямую, acc += x компилируется в обычный ADD, без всякого словаря на горячем пути и без боксинга. Поэтому на value-типах дженерик близок к рукописи.
Резюме механизма одной таблицей:
| Аспект | C++ (мономорфизация) | Java (стирание) | Go (shape stenciling + dict) |
|---|---|---|---|
| Копий кода | на каждый тип | одна | одна на форму |
| Value-типы | прямой код, инлайн | боксинг | специализация по форме, без боксинга |
| Вызов метода | прямой/инлайн | virtual | косвенно через словарь |
| Размер бинаря | раздувается | компактно | умеренно (копии шарятся по форме) |
| Рантайм-инфо | нет | приведения | словарь |
Цена: бенчмарки на живом стенде
Теория выше проверяема. Всё, что дальше, заземлено на живой стенд digital-cookbook/generics/ — чистый Go без docker и внешних зависимостей, три сюжета в трёх пакетах. Числа сверены прогоном на go1.26.3 windows/amd64 (go test -bench=. -benchmem -count=3 ./...). Они ориентировочные — зависят от машины, версии Go и флагов; снимайте у себя. Робастны здесь не абсолютные наносекунды, а порядки и аллокации. Про то, как правильно снимать такие числа и не обмануться, — отдельная статья про профилирование и бенчмарки.
Сюжет 1 — value-типы: дженерики выигрывают
Три версии суммы числового среза: дженерик SumGeneric[T Number], рукописные SumInt/SumFloat64 и SumIface([]any) с боксингом. Важная оговорка про последнюю: её бенчмарк меряет стоимость интерфейсного подхода целиком — включая конвертацию []int/[]float64 в []any (boxInts/boxFloats) прямо в теле цикла. Это честный сквозной сценарий «у меня на руках typed slice, а API хочет []any», но именно поэтому цифры такие большие: платим и за упаковку каждого элемента, и за сам dispatch. Не читайте это как «любой interface-вызов на два порядка медленнее» — так дорого выходит конкретно массовый боксинг value-типов. Результаты:
| Бенчмарк | ns/op | B/op | allocs/op | Что показывает |
|---|---|---|---|---|
SumInt_Generic |
~1400 | 0 | 0 | дженерик по форме int |
SumInt_Handwritten |
~3200 | 0 | 0 | рукописный эталон int |
SumInt_Iface |
~100000 | 96256 | 3841 | боксинг int → аллокации |
SumFloat64_Generic |
~3300 | 0 | 0 | дженерик по форме float64 |
SumFloat64_Handwritten |
~3700 | 0 | 0 | рукописный эталон float64 |
SumFloat64_Iface |
~95000 | 98296 | 4096 | боксинг float64 → аллокации |
Читаем аккуратно. Дженерик и рукопись — один порядок и ноль аллокаций в обоих случаях: специализация по форме работает, боксинга нет. Разница дженерик/рукопись на int (≈1400 против ≈3200) выглядит так, будто дженерик быстрее рукописи — но это артефакт инлайна и кодогена на конкретной сборке, а не общее правило. Не стройте на этом выводов: корректная формулировка — «дженерик и рукопись сопоставимы по скорости и оба не аллоцируют».
А вот interface{}-версия — в десятки раз дороже (≈100µs против ≈1–3µs) и боксит: ~3841 аллокаций на int (~96 КБ/op) и ~4096 на float64. Каждый элемент упаковывается в интерфейсное значение; для целых ≥256 упаковка идёт с аллокацией на куче (значения <256 рантайм кэширует, потому и не ровно 4096). Вот она, реальная цена any на горячем пути над value-типами — и вот где дженерики её убирают.
Сюжет 2 — указательные типы: дженерик НЕ быстрее интерфейса
Ключевой сюрприз. Дженерик SumAreasGeneric[T Shape] вызывает x.Area() через constraint по указательным *Circle/*Rect; интерфейсная SumAreasIface([]Shape) делает то же через обычный dispatch.
| Бенчмарк | ns/op | B/op | allocs/op | Что показывает |
|---|---|---|---|---|
Area_Generic |
~12300 | 0 | 0 | вызов Area() через словарь специализации |
Area_Iface |
~12900 | 0 | 0 | вызов Area() через интерфейсную таблицу (itab) |
≈12.3µs против ≈12.9µs, оба — ноль аллокаций: в этом Windows-прогоне дженерик близок к интерфейсу и точно не быстрее. Но «близость» машинозависима — на других системах дженерик тут бывает и заметно медленнее интерфейса; устойчив лишь вывод, что ускорения ждать нельзя. Причина ровно та, что разобрана под капотом: все указатели — одна форма, делят одну копию кода, вызов Area() идёт косвенно через словарь, девиртуализации нет. Если вы тянете дженерик в такой код в надежде на ускорение — надежда не оправдается. Брать его тут стоит ради типобезопасного API, а не ради скорости.
Сюжет 3 — размер бинаря: никакого раздувания
Два бинаря: generic инстанцирует дженерик-функции для дюжины типов (int, int8/16/32/64, uint*, float32/64, string), iface делает то же через одну SumTwoIface(any, any) any.
| Бинарь | Размер | Комментарий |
|---|---|---|
bin/generic |
2 467 328 Б | инстанцирование дженерика по многим формам |
bin/iface |
2 467 328 Б | одна interface{}-функция |
| разница | 0 байт в этом прогоне (Windows) | нет раздувания; на др. ОС/версиях возможна малая разница |
Совпадение байт-в-байт получилось в этом конкретном прогоне (Windows, go1.26.3); на других ОС/версиях бинари могут разойтись на сотни байт из ~2.4 МБ — важен не точный ноль, а порядок: раздувания нет. В C++ аналогичный шаблон, инстанцированный по дюжине типов, дал бы дюжину копий кода и заметный рост. Go так не делает — это осознанный размен: чуть медленнее (словари, косвенность) в обмен на компактность и быструю компиляцию.
Когда дженерики выигрывают, когда нет
Соберём выводы в решающее правило.
Дженерики дают выигрыш — берите:
- Контейнеры и алгоритмы над value-типами. Сумма/сортировка/поиск по
[]int,[]float64, множества, стеки, очереди — специализация по форме убирает боксингanyи даёт скорость рукописи. Это сюжет 1. - Type-safe API вместо
interface{}. Даже там, где скорость не меняется, дженерик убирает приведения и ловит ошибки типов на компиляции.Stack[T]лучшеStackнаanyне потому, что быстрее, а потому, что безопаснее. - Утилиты stdlib.
slices,maps,cmp— используйте их вместо самописныхany-хелперов и вместо кодогена.
Выигрыша по скорости нет — берите только ради типобезопасности или не берите:
- Код с интерфейсным dispatch по указателям. Если внутри всё равно вызов метода через constraint по указательному типу — это сюжет 2: дженерик = интерфейс по стоимости. Берите дженерик, если он делает API чище/безопаснее; но не ждите ускорения, и если он усложняет сигнатуры — честнее обычный
interface{}. - Единичное место, где
anyчитается проще. Дженерик с тремя параметрами типа и вложенными constraint’ами бывает менее читаем, чем прямолинейныйany+ type switch. Читаемость — тоже инженерный критерий. - Массовая специализация под перформанс-критичный числовой код. Иногда честнее кодоген или ручная специализация под конкретные типы, если вы точно знаете набор типов и выжимаете последние проценты, — но это редкий случай, начинать надо с дженерика.
Одной фразой: дженерики берут ради типобезопасности — когда обобщение действительно упрощает API, — а ради скорости только на value-типах. Там, где конкретная функция или обычный интерфейс дают более читаемый код, они и уместнее: обобщение ради обобщения — не цель. Смежная тема выбора модели исполнения — «Модели конкурентности across languages»; а идиоматичные способы структурировать сам код на Go — в «Паттернах конкурентности Go».
Границы механизма
Дженерики Go сознательно ограничены — часть того, что есть в C++/Java/Rust, здесь просто нельзя. Знать границы важно, чтобы не биться о них в дизайне.
- Нет дженерик-методов у не-дженерик типов. Метод не может ввести собственный параметр типа.
func (r Repo) Get[T any](id int) T— не компилируется. Параметризовать можно только тип целиком (type Repo[T any]) или свободную функцию, но не отдельный метод существующего типа. Причин несколько (сложность инстанцирования методов, method sets, интерфейсы и линковка), и словари — одна из них: метод пришлось бы диспетчеризовать по типу, неизвестному при объявлении. Полный разбор — в type-parameters proposal.
type Cache struct{ /* ... */ }
// func (c *Cache) Get[T any](key string) T { ... } // ОШИБКА компиляции
// Обходной путь — свободная дженерик-функция:
func CacheGet[T any](c *Cache, key string) T {
var z T
// ... достать из c и привести ...
return z
}- Нельзя параметризовать метод отдельно от типа. Из предыдущего: набор параметров типа фиксируется на объявлении типа/функции, «доп.
T» на вызове метода не добавить. - Нет ковариантности.
[]Tне является[]Shape, даже если каждыйTреализуетShape.Stack[*Circle]— неStack[Shape]. Дженерик-типы инвариантны по параметру. Хотите гетерогенную коллекцию — используйте[]Shape(интерфейс), а не дженерик. - Constraints не дают доступа к полям. Union типов в constraint разрешает операции (
+,<,==) и методы, но не поля структур. Нельзя написать constraint «любой тип с полем.Name string» и обратиться кx.Nameвнутри дженерика. Общий доступ к данным — только через методы-геттеры в интерфейсе-constraint. Это регулярно всплывает у тех, кто ждёт «structural typing по полям», как в TypeScript, — в Go его нет.
Эти границы — не недоработка, а следствие выбранной реализации и приоритета простоты. Полное сравнение того, что каждый язык позволяет и запрещает, — в компаньоне «Дженерики across languages».
Демо и версии
- Код: живой стенд
digital-cookbook/generics/— чистый Go, без docker и внешних зависимостей, один модуль, три подкаталога по сюжетам:numeric/(value-типы — дженерик vs рукопись vsinterface{}),dispatch/(указательные типы — дженерик vs интерфейс через constraint),binsize/(размер бинаря). Бенчмарки —go test -bench=. -benchmem ./...; размеры бинарей — сперваmkdir -p bin(каталог в чистом checkout не создан), затемgo build -o bin/generic ./binsize/cmd/genericи то же дляiface, после чего сравнить. Код в тексте — выжимки из стенда. - Версии: дженерики доступны с Go 1.18 (type parameters, constraints,
~,comparable). С Go 1.20 обычные интерфейсы стали удовлетворятьcomparable(сравнение может паниковать). С Go 1.21 появились стандартныеslices,maps,cmp— предпочитайте их самописнымany-хелперам иx/exp/constraints. - Числа сверены на
go1.26.3 windows/amd64и ориентировочны (машина/версия/флаги влияют). Робастные выводы: дженерик ≈ рукопись на value-типах (один порядок, 0 аллокаций),interface{}дороже в десятки раз с тысячами аллокаций от боксинга, на указательных типах дженерик не быстрее интерфейса, а размер бинаря — без раздувания в порядке величины (в Windows-прогоне байт-в-байт, на Linux — разница ~500 Б из ~2.4 МБ). Снимайте у себя — не переносите абсолютные ns из статьи в свои решения. - Смежное: «Дженерики across languages» (языко-агностичное сравнение подходов), профилирование и бенчмарки в Go (как честно измерять), паттерны конкурентности Go, модели конкурентности across languages.
Документация и первоисточники
- Go blog: An Introduction to Generics — вводная от команды Go: type parameters, constraints, инференс, и правило «если реализация метода различается по типам — это интерфейс, а не дженерик».
- Go blog: When To Use Generics — когда дженерики уместны, а когда лучше интерфейс или обычный код; практические критерии выбора.
- Type Parameters Proposal — исходный design doc дженериков: полная семантика constraint’ов,
~-approximation, union-элементов, инференса и границ механизма. - PlanetScale blog: Generics can make your Go code slower — практический разбор того, почему на указательных типах дженерик идёт через словарь и не девиртуализуется (тот самый сюрприз из сюжета 2).
- Go 1.18 Release Notes — релиз дженериков; раздел о реализации через GC-shape stenciling и словари (dictionaries).
- Исходники компилятора: пакет
cmd/compile/internal/noder(reader.go/writer.go— генерация копий по форме и сборка словарей) иcmd/compile/internal/devirtualize— где девиртуализация всё же происходит.
Комментарии