---
title: "Дженерики: обобщённые функции и типы"
description: "Дженерики в Go: type parameters, constraints comparable и cmp.Ordered, union-констрейнты, обобщённый Stack[T], вывод типов и когда дженерики не нужны."
course: "Продвинутые возможности Go: от замыканий до рефлексии"
url: "https://golangmaster.ru/courses/go-advanced/lessons/13-dzheneriki/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 24
---

# Дженерики: обобщённые функции и типы

*Курс «Продвинутые возможности Go: от замыканий до рефлексии», урок 13 из 25. ~24 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-advanced/lessons/13-dzheneriki/*

Дженерики go — запрос, с которым приходят после одиннадцатого и двенадцатого уроков: мы научились класть что угодно в коробку any и доставать обратно утверждением типа, но заплатили переносом проверки в рантайм. Дженерики — type parameters — возвращают типы в сигнатуру: func Max[T cmp.Ordered](a, b T) T объявляет «пока неизвестный тип T с контрактом», и компилятор сверяет всё на этапе сборки. В уроке: зачем дженерики нужны (три копии одной функции против одной обобщённой), констрейнты any, comparable и union, cmp.Ordered, вывод типов Max(1, 2) без [int], обобщённые типы Stack[T] и случаи, когда дженерики не нужны. Все примеры запускаются прямо на странице — в основном движке песочницы; где резервный движок пасует, честно сказано в тексте и в плашке урока.

### Дженерики в Go: одна функция — много типов

К марту 2022 года Go был единственным крупным языком без обобщённого программирования: с версии 1.0 по 1.17 язык прожил без дженериков, и сообщество писало генераторы кода, копии функций и коробки any. Версия 1.18 привнесла **type parameters** — параметры типов — и это самое большое расширение языка за всю его историю. В англоязычной документации тема называется generics (запрос **generics golang** — он и приводит на эту страницу), по-русски говорят «обобщённые функции», «параметризованные типы», «тип-параметры». Всё это об одном: **одна функция или один тип — и список типов, с которыми он работает**.

У нас уже есть три инструмента для лозунга «одна функция — много типов», и каждый платит свою цену. Интерфейсы дал десятый урок, коробку any — [одиннадцатый](/courses/go-advanced/lessons/11-polimorfizm/), инструменты распаковки — [двенадцатый](/courses/go-advanced/lessons/12-type-assertion/). Интерфейсы дают полиморфизм поведения: любой тип с нужными методами подходит, но тело функции видит только методы контракта. any — контракт без единого метода: положить можно всё, работать с содержимым — ничего. Type assertion — распаковка коробки: проверка динамического типа уходит в рантайм, а ошибка стоит паники interface conversion или флага ok. Дженерики — четвёртый инструмент, и их цена другая: **проверка возвращается компилятору**. Тип-параметр T живёт в сигнатуре, конкретный тип подставляется при вызове, и ни пары «динамический тип + значение», ни утверждения, ни рантайм-проверки не нужно.

План урока: сначала боль, которую лечат дженерики, — три копии одной функции; затем анатомия type parameters и первый обобщённый Max; вывод типов — почему Max(1, 2) собирается без [int]; констрейнты any, comparable и union; cmp.Ordered и судьба знаменитого пакета constraints; обобщённые типы на примере Stack[T]; и развилка, на которой дженерики не нужны вовсе. Все интерактивы урока исполняются основным движком песочницы (настоящий yaegi) и локальным go run; первый раннер работает в обоих движках, остальные — только в основном, и почему — честно разобрано в плашке перед финалом.

### Боль до 1.18: три копии одной функции

Сформулируем задачу так, как её формулировали каждый день до версии 1.18. Нужна функция «большее из двух» — для целых, для строк, для дробных. Дженериков нет, интерфейсы не помогают: у int нет методов, контракт «умеет сравниваться» в Go не существовал, а any внутри — это коробка, из которой арифметику не достать ([одиннадцатый урок](/courses/go-advanced/lessons/11-polimorfizm/) показал это на `box[0] + 1`, который не собирается). Оставалось два честных пути.

**Путь первый — копии.** maxInt(a, b int), maxString(a, b string), maxFloat64(a, b float64) — три функции с идентичными телами, отличающиеся только типами в подписи. Тело маленькое — потеря терпима; но представьте не три строки, а сорок: сорок копий сорок раз правятся при каждой ошибке, и компилятор не напомнит, что вы забыли одну.

**Путь второй — any + assertion.** Одна функция maxAny(a, b any) any: внутри ok-формы «вы это int?», «вы это string?» — каскад утверждений из двенадцатого урока, при неудаче — паника или флаг. Цена: проверка уходит в рантайм, а возвращённое значение — снова any, и вызывающему распаковывать его ещё раз. Ошибка «передали не тот тип», которую компилятор ловит на обычных функциях мгновенно, у maxAny обнаружится в три часа ночи на проде.

Оба пути платят за одну и ту же вещь: **отсутствие типа в сигнатуре**. Копии платят дублированием, any — безопасностью. Дженерики снимают плату целиком: тип остаётся неизвестным на момент написания функции, но известным — и проверенным — на момент вызова. Раннер ниже показывает первый путь: запустите его, посмотрите на три идентичных тела, а дальше урок напишет то же самое одной функцией.

### Три копии вместо одной: как было до дженериков

Запустите: вывод «9 кот 2.5» — три функции, три одинаковых тела, три подписи. Если завтра в сравнении найдётся ошибка, править придётся в трёх местах, и компилятор не подскажет, что вы пропустили четвёртую копию для rune. Эксперимент: допишите maxRune(a, b rune) rune и вызовите её — ещё одна копия на ещё один тип. Этот раннер намеренно написан без дженериков: он исполняется обоими движками песочницы — и основным, и резервным; все остальные интерактивы урока содержат квадратные скобки type parameters, которые резервный движок не парсит вовсе.

```go
package main

import "fmt"

// Три копии одной и той же функции: отличается только тип в подписи.
func maxInt(a, b int) int {
        if a > b {
                return a
        }
        return b
}

func maxString(a, b string) string {
        if a > b {
                return a
        }
        return b
}

func maxFloat64(a, b float64) float64 {
        if a > b {
                return a
        }
        return b
}

func main() {
        fmt.Println(maxInt(3, 9), maxString("кот", "кит"), maxFloat64(2.5, 1.5))
}
```

### Анатомия type parameters: квадратные скобки

Вот та же функция, написанная один раз:

`func Max[T cmp.Ordered](a, b T) T { if a > b { return a }; return b }`

Читаем подпись по членам, слева направо. **Имя** — Max. Затем **квадратные скобки** `[T cmp.Ordered]` — список тип-параметров; это новая секция синтаксиса, которой до 1.18 не было. Внутри: `T` — имя тип-параметра (традиция: одна буква, как у переменной цикла; в больших сигнатурах встречаются T, R, K, V — тип и результат, ключ и значение). `cmp.Ordered` — **констрейнт**, контракт тип-параметра: «T — какой-нибудь тип, который умеет сравниваться знаками < и >». Затем обычные круглые скобки: параметры `a, b T` — оба выражены через тип-параметр, то есть их тип станет известен при вызове. Последнее `T` — тип результата, тоже параметризованный.

Ключевое отличие от any — **где живёт неизвестность**. В `func maxAny(a, b any) any` неизвестность уже внутри: любые операции над a, кроме самых общих, не скомпилируются, а возвращённая коробка требует распаковки. В `func Max[T cmp.Ordered](a, b T) T` неизвестность — в подписи, и она **ограничена контрактом**: тело вправе применять к a и b всё, что обещает cmp.Ordered, — сравнение, равенство — и ни одной операции сверх. При вызове `Max(3, 7)` компилятор подставит конкретный тип (int), соберёт отдельную версию функции и проверит тело уже с ним: ошибка «строку сравнили знаком больше, а строки так не умеют» — у народа, который обещает только равенство, — будет найдена на этапе сборки, а не в рантайме.

Процесс подстановки конкретного типа вместо T называется **инстанциацией**. Она бывает неявной — компилятор сам выводит тип из аргументов (вывод типов, ниже) — и явной: `Max[int](3, 7)`. Квадратные скобки на месте вызова — это вы, а не компилятор, называете тип; они же спасают, когда вывод неоднозначен.

### Max[T cmp.Ordered]: одна функция вместо трёх

Запустите: вывод «7, кот, 20, y» — четыре вызова, одна функция. Первые два вызова — вывод типов: компилятор сам подставил int и string по аргументам. Третий и четвёртый — явная инстанциация: тип назван в квадратных скобках на месте вызова, вывод не нужен. Заметьте: сравнение строк через > легально, потому что контракт cmp.Ordered обещает упорядоченность, а строки в Go упорядочены лексикографически. Эксперимент: вызовите Max(false, true) — не соберётся: bool не входит в контракт Ordered, у булевых нет порядка. Резервный движок песочницы не парсит квадратные скобки после имени функции и отвечает ошибкой вида «ожидалось "(", получено "["»; основной движок и локальный go run работают честно.

```go
package main

import (
        "cmp"
        "fmt"
)

// Max — одна функция для всех упорядоченных типов.
func Max[T cmp.Ordered](a, b T) T {
        if a > b {
                return a
        }
        return b
}

func main() {
        fmt.Println(Max(3, 7))            // вывод типов: T — int
        fmt.Println(Max("кот", "кит"))    // вывод типов: T — string
        fmt.Println(Max[int](10, 20))     // явная инстанциация
        fmt.Println(Max[string]("x", "y"))
}
```

### Вывод типов: Max(1, 2) без [int]

В первом раннере мы вызвали `Max(3, 7)` — и нигде не написали int. Это **вывод типов** (type inference): компилятор смотрит на типы аргументов, сопоставляет их с тип-параметрами сигнатуры и сам решает, что T — int. Механика называется унификацией: аргументы int и int, параметр T — значит T = int; одна переменная выводится из всех своих использований сразу, и они обязаны согласиться. `Max(3, 7)` — T = int; `Max("кот", "кит")` — T = string; `Max(1, "x")` — вывод не согласован, ошибка компиляции: один T не может быть одновременно int и string, приводить типы за вас компилятор не станет.

Правило курса: **пишите вызов без скобок, пока вывод справляется**. Явная инстанциация `Max[int](3, 7)` нужна в трёх случаях. Первый — вывод невозможен: тип не встречается в аргументах (результатный R у `func Pair[T, R any](a T, b R)` выводится из b — а вот если R встречается только в результате, его придётся назвать). Второй — аргументы — нетипизированные константы и вывод спорит: с union-констрейнтом ниже вы увидите это вживую. Третий — вы хотите сами назвать тип ради читателя или ради цепочки вызовов, где дальше важен конкретный тип.

И честная нота про песочницу, проверенная пробником до написания урока. Основной движок (yaegi) выводит целые нетипизированные константы нормально: `Max(3, 7)` работает. А вот **дробные** нетипизированные константы он не выводит: `Max(2.5, 1.5)` отвечает «untyped float does not implement cmp.Ordered», хотя настоящий Go спокойно выводит T = float64. Обхода два — типизированная переменная (`fx, fy := 2.5, 1.5`, затем `Max(fx, fy)`) или явная инстанциация `Max[float64](2.5, 1.5)`. Локальный `go run` выводит дробные константы честно — это ограничение именно песочницы, и оно разобрано в плашке урока.

### Проверьте себя

**Вопрос:** func Max[T cmp.Ordered](a, b T) T. Что напечатает fmt.Println(Max(3, 7)) и какой тип получит T?

- ✅ 7; компилятор выведет T = int из типов аргументов
- 7; T останется абстрактным — any внутри функции
- 3; Max вернёт первый аргумент
- Ошибка компиляции: тип T надо указать явно — Max[int](3, 7)

Вывод типов сопоставляет аргументы с тип-параметром: оба int — значит T = int, и никакого any внутри нет: тело работает с настоящими int, сравнение > легально, результат — int. Явная инстанциация Max[int](3, 7) тоже соберётся, но необязательна: вывод справляется. Вернулся бы 3, только если бы тело возвращало a, а не большее.

### Констрейнты: контракт, который видит компилятор

**Констрейнт** — это обещание о тип-параметре, записанное в сигнатуре. Тело функции вправе применять к значениям T ровно те операции, которые обещает констрейнт, — и ни одной больше. Компилятор проверяет тело против констрейнта один раз (при сборке функции), а каждый вызов — против констрейнта же (подходит ли конкретный тип обещанию). Это двусторонний договор: и телу, и вызывающему известно одно и то же.

Минимальный констрейнт — **any**: `func Dup[T any](v T) (T, T)` — «T — какой-нибудь тип, обещаний нет». Внутри такой функции над значениями T нельзя ни сравнивать, ни складывать, ни упорядочивать — можно хранить, передавать, возвращать, брать `var zero T` (нулевое значение неизвестного типа — int даст 0, string — пустую строку). any-дженерик полезен для контейнеров и обёрток: пара (T, T) из Dup, срезы, стеки. Как только телу нужна операция — поднимайте контракт.

Констрейнт **comparable** обещает равенство: значения типа T можно сравнивать знаками `==` и `!=`. Это встроенный контракт, и он точен до синтаксиса: под обещание подходят все сравнимые типы — числа, строки, булевы, указатели, каналы, интерфейсы, а также структуры и массивы, все поля и элементы которых сравнимы. Срезы, карты и функции сравнимыми не являются. Зачем это нужно? Вся арифметика проверки существования: `func Contains[T comparable](s []T, v T) bool` — линейный поиск, `func Equal[T comparable](a, b T) bool`, ключи карт `map[T]int` — тип-параметр ключа обязан быть comparable, потому что карта сравнивает ключи. Напомним язык двенадцатого урока: assertion спрашивал «ты int?» в рантайме и платил паникой; comparable обещает «ты умеешь ==» на этапе компиляции — вопрос снят до запуска.

Важно, чего comparable НЕ обещает: порядка. `a < b` над T с контрактом comparable не скомпилируется — обещание «равенство» не покрывает «меньше-больше». Для порядка нужен контракт со сравнением — cmp.Ordered или union, о них ниже. Правило выбора простое: берите **минимальный констрейнт, достаточный телу** — чем меньше обещаний, тем больше типов сможет вызвать функцию.

### Проверьте себя ещё раз

**Вопрос:** func Equal[T ?](a, b T) bool { return a == b } — какой констрейнт должен стоять вместо вопроса, чтобы функция собралась?

- any: пустой контракт разрешает любые операции над T
- ✅ comparable: гарантирует поддержку == и != — минимальный достаточный контракт
- Никакой: операция == работает над T при любом констрейнте
- comparable не существует: для == нужен union из конкретных типов

any обещает ровно ничего: выражение a == b над T с констрейнтом any не компилируется — компилятор не вправе предполагать, что тип умеет равенство. comparable — встроенный контракт именно про равенство: под него подходят числа, строки, булевы, указатели и структуры из сравнимых полей. cmp.Ordered тоже включает == (Ordered расширяет comparable), но обещает вдобавок порядок — лишнее для Equal, минимальный достаточный контракт предпочтительнее. Union из конкретных типов соберётся тоже, но перечислять типы вручную там, где есть штатный comparable, — работа ни к чему.

### Union-констрейнт: только int и float64

Запустите: вывод «5, 3.75, 0.75». Первая строка — вывод типов с целыми константами: T = int, сложение легально, потому что int входит в констрейнт. Вторая — типизированные переменные fx и fy: T = float64. Третья — явная инстанциация Sum[float64]. Констрейнт-union записывается как интерфейс, но тело которого — перечисление типов через |: такой интерфейс нельзя реализовать методами, он — буквально «множество допустимых типов». Эксперимент: вызовите Sum(2, 3.5) — не соберётся: один T не может быть и int, и float64. Резервный движок не парсит вертикальную черту в интерфейсе («ожидался идентификатор, получено "|"»); основной движок и go run работают. Квирк основного: нетипизированные дробные константы Sum(1.5, 2.25) он не выводит — пишите типизированные переменные или Sum[float64](...).

```go
package main

import "fmt"

// Number — самодельный констрейнт: множество типов через вертикальную черту.
type Number interface {
        int | float64
}

// Sum складывает два числа одного типа T.
func Sum[T Number](a, b T) T {
        return a + b
}

func main() {
        fmt.Println(Sum(2, 3)) // вывод типов: T — int
        fx, fy := 1.5, 2.25    // типизированные float64
        fmt.Println(Sum(fx, fy))
        fmt.Println(Sum[float64](0.5, 0.25)) // явная инстанциация
}
```

### Union-констрейнты и три пути к Ordered

Union — констрейнт, записанный перечислением типов через вертикальную черту. Синтаксис нарочно использует слово interface: `type Number interface { int | float64 }`. Это интерфейс нового образца — **не контракт поведения, а множество типов**: у него нет ни одного метода, его нельзя «реализовать», в него входят ровно перечисленные типы. Тело `func Sum[T Number](a, b T) T` вправе делать с a и b то, что умеют ОБА типа множества: сложение, вычитание, сравнение; делить на строку — не вправе, строки в множестве нет. Косая черта читается «или», а унификация вывода работает так же, как раньше: `Sum(2, 3)` — int, `Sum(fx, fy)` с типизированными float64 — float64.

Нюанс, который стоит знать с самого начала: `int | float64` — это ровно эти два типа, и named-типы поверх них (type Celsius float64) в множество не входят. Чтобы включить «все типы, чьим базовым является float64», пишут волну: `~float64`. В наших примерах достаточно голых имён, но в библиотечном коде встретите тильду постоянно.

Теперь три пути к констрейнту «упорядоченный» — история, в которой запутаться легче всего, поэтому распутываем здесь. **Путь первый — самодельный union**: `type Ordered interface { int | float64 | string }` — вы перечисляете типы вручную; в песочнице это работает, но список длинный (в Ordered входят все целые и дробные размеры). **Путь второй — cmp.Ordered**: с версии Go 1.21 в стандартной библиотеке живёт пакет cmp, и его констрейнт `cmp.Ordered` обещает ровно нужное — все целые, все дробные и строки, то есть `<`, `>`, `==` и company. Наш раннер с Max использует именно его, и песочница его импортирует без проблем — проверено пробником. **Путь третий — golang.org/x/exp/constraints**: знаменитый внешний пакет, с которого всё началось до 1.21; `constraints.Ordered` из него и был образцом для stdlib. В песочнице этот пакет импортировать нельзя — основной движок отвечает «unable to find source related to: golang.org/x/exp/constraints» (проверено пробником до написания урока): внешний модуль в песочницу не установлен. Локально он работает после `go get golang.org/x/exp`, но новому коду рекомендуют stdlib-версию — cmp.Ordered. В тексте и комментариях урока слово constraints остаётся общим термином для «контракта тип-параметра», а конкретные констрейнты мы пишем самодельным union-ом или cmp.Ordered.

### Что выведет Max на числах и строках?

```go
package main

import (
        "cmp"
        "fmt"
)

func Max[T cmp.Ordered](a, b T) T {
        if a > b {
                return a
        }
        return b
}

func main() {
        fmt.Println(Max(3, 7))
        fmt.Println(Max("кит", "кот"))
        fx, fy := 1.5, 2.5
        fmt.Println(Max(fx, fy))
}
```

**Что выведет программа?**

- ✅ 7 / кот / 2.5 — в каждой строке вывелся больший аргумент
- 7 / кит / 2.5 — Max вернул первый аргумент
- 7 / кот / 1.5 — для дробных Max вернул меньший
- Не собирается: для строк и чисел нужны отдельные функции

cmp.Ordered покрывает целые, дробные и строки — сравнение > легально для всех трёх вызовов. Строки упорядочены лексикографически по байтам: «о» (0x43E) идёт после «и» (0x438), поэтому «кот» больше «кит». Для дробных аргументы — типизированные переменные fx, fy float64, вывод даёт T = float64, большим оказывается 2.5. Настоящий Go и основной движок печатают «7, кот, 2.5» одинаково; резервный движок дженерики не парсит вовсе.

### Обобщённые типы: type Stack[T any] struct

Type parameters живут не только у функций — у **типов** тоже. `type Stack[T any] struct { items []T }` объявляет обобщённый стек: T — тип-параметр на уровне всего типа, и внутри объявления срез `items` хранит элементы именно T. Это то, чего any дать не мог в принципе: `Stack{ items: []any{...} }` — коробка, в которую кладётся что угодно и из которой ничего не достаётся без assertion; `Stack[string]` — конкретный тип, куда int не положит **компилятор**, а не рантайм.

Методы обобщённого типа получают приёмник с тем же параметром: `func (s *Stack[T]) Push(v T)` — читается «метод Push у Stack с элементами T». Обратите внимание: внутри приёмника и сигнатуры метода T уже не объявляется — он объявлен у типа, и методы лишь используют его. Указательный приёмник здесь не прихоть: Push меняет стек на месте, добавляя в срез items.

Инстанциация — та же, что у функций: `Stack[string]{}` — композитный литерал уже конкретного типа; `var s Stack[int]` — нулевой стек для int. Два инстанцирования — два разных типа: Stack[int] и Stack[string] не присваиваются друг другу, хотя написаны одной декларацией. Это цена и сила: одна логика — и полная типовая дисциплина внутри каждой версии.

Внутри метода встретится приём, достойный отдельного абзаца: `var zero T` — нулевое значение неизвестного типа. Pop на пустом стеке обязан вернуть «ничего», но какого типа? Типа T — и `var zero T` даёт ровно его: для int это 0, для string — "", для указателя — nil. Без any-коробки, без утверждения, без паники: язык сам знает нулевое значение каждого типа, который подставится. Второе значение ok в паре `Pop() (T, bool)` — та же идиома «комма-ок», что у чтения канала и type assertion: результат и флаг честности рядом.

### Stack[string]: Push, Pop и пустой стек

Запустите: «сняли: два true, сняли: три true, сняли: раз true, пустой стек: false». LIFO в чистом виде: вышли «два», потом «три», потом «раз». Четвёртая попытка — Pop на пустом стеке: var zero T дал пустую строку, флаг false честно сказал «элемента не было» — та же комма-ок-идиома, что у чтения закрытого канала и ok-формы type assertion. Эксперимент: замените Stack[string] на Stack[int] и кладите числа — одна декларация Stack[T any] обслужит и строки, и числа, а int вместо string в Push не соберётся: типовая дисциплина на компиляции. Резервный движок не парсит type Stack[T any] («ожидалось "]", получено "any"») — этот раннер для основного движка и go run.

```go
package main

import "fmt"

// Stack — обобщённый тип: стек элементов типа T.
type Stack[T any] struct {
        items []T
}

// Push кладёт элемент на вершину: указательный приёмник меняет стек на месте.
func (s *Stack[T]) Push(v T) {
        s.items = append(s.items, v)
}

// Pop снимает вершину: второе значение — был ли элемент.
func (s *Stack[T]) Pop() (T, bool) {
        if len(s.items) == 0 {
                var zero T
                return zero, false
        }
        v := s.items[len(s.items)-1]
        s.items = s.items[:len(s.items)-1]
        return v, true
}

func main() {
        s := Stack[string]{}
        s.Push("раз")
        s.Push("два")
        v, ok := s.Pop()
        fmt.Println("сняли:", v, ok)
        s.Push("три")
        w, ok2 := s.Pop()
        fmt.Println("сняли:", w, ok2)
        last, ok3 := s.Pop()
        fmt.Println("сняли:", last, ok3)
        _, empty := s.Pop()
        fmt.Println("пустой стек:", empty)
}
```

### Соберите union-констрейнт и Sum

Впишите: первый пропуск — целый тип в множестве Number, второй — имя тип-параметра в квадратных скобках, третий — знак операции в теле.

```go
package main

import "fmt"

type Number interface {
        ___ | float64
}

func Sum[___ Number](a, b T) T {
        return a ___ b
}

func main() {
        fmt.Println(Sum(2, 3))
}
```

Подсказка: Множество типов перечисляется через вертикальную черту — слева от | стоит целый тип; имя тип-параметра — одна заглавная буква, традиционно T; складывать в Go умеет знак плюс.

Правильные ответы в пропусках: `int`, `T`, `+`

### Соберите объявление обобщённого типа

Соберите программу: обобщённый стек Stack[T], инстанцирование для int и добавление элемента.

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `type Stack[T any] struct {`
4. `items []T`
5. `}`
6. `func main() {`
7. `s := Stack[int]{}`
8. `s.items = append(s.items, 7)`
9. `fmt.Println(s.items)`
10. `}`

### Когда дженерики НЕ нужны

Дженерики — самый громкий инструмент языка, и громкость провоцирует злоупотребление: как только они появились, код начал обрастать `[T any]` там, где нужен был обычный int. Четыре случая, когда дженерики не нужны — и что писать вместо них.

**Случай первый: тип на самом деле один.** Если функция работает только с int — напишите её с int. `func maxInt(a, b int) int` короче, читается мгновенно, и вывод типов не нужен никому. Обобщение ради обобщения добавляет квадратные скобки в каждую подпись и третий вопрос к каждому читателю («а что тут может быть?»). Правило: пишите конкретный тип, пока конкретный тип справляется; дженерик — когда появился ВТОРОЙ тип с той же логикой.

**Случай второй: различается поведение, а не тип.** Если «разные типы» на самом деле означают «разные действия» — это территория интерфейсов из [одиннадцатого урока](/courses/go-advanced/lessons/11-polimorfizm/): контракт поведения, duck typing, полиморфизм методов. fmt.Stringer, io.Reader, error — интерфейсы, и дженерики их не заменяют: generic-функция делает ОДНО и то же с разными типами, интерфейсная — РАЗНОЕ с одним контрактом.

**Случай третий: данные по-настоящему разнородны.** Коробка []any из одиннадцатого урока и type switch из двенадцатого остаются правильным инструментом там, где типы элементов неизвестны ВООБЩЕ — парсинг JSON, плагины, обработчики с внешним контрактом. Дженерик требует, чтобы тип был известен на момент вызова; если не известен — дженерик не про это.

**Случай четвёртый: хватает any.** `func Dup[T any](v T) (T, T)` честнее написать `func Dup(v any) (any, any)`, если вызывающему всё равно придётся распаковывать: тип-параметр без операций в теле и без использования связей (a и b одного T) — это any с лишними скобками. Дженерик становится сильнее any, когда T встречается в сигнатуре ДВАЖДЫ и больше — «вернёт тот же тип, что принял», «примет срез из T, вернёт T» — связь, которую any не выражает никогда. Стандартная библиотека до сих пор наполовину не-дженерик: fmt.Println принимает any, а slices.Contains и slices.Sort — дженерики; каждый инструмент на своём месте.

### Шпаргалка: type parameters и констрейнты

| Инструмент | Что обещает | Где применять | Осторожно |
| --- | --- | --- | --- |
| [T any] | какой угодно тип, обещаний нет | контейнеры, хранение и передача, var zero T | арифметика, == и < над T не скомпилируются |
| [T comparable] | равенство: == и != | Equal, Contains, ключи map[T]V | порядка нет: < и > недоступны; срезы и карты не сравнимы |
| [T int | float64] | операции, общие для перечисленных типов | Sum и арифметика узкого множества | named-типы поверх int не входят: нужна тильда ~int |
| [T cmp.Ordered] | упорядоченность: ==, <, > и остальные | Max, Min, сортировка | с Go 1.21; раньше — x/exp/constraints, в песочнице его нет |
| вывод типов Max(1, 2) | T берётся из аргументов вызова | пишите без скобок, пока вывод справляется | один T из двух разных типов не выведется; дробные константы — квирк песочницы |
| type Stack[T any] struct | обобщённый тип с методами (s *Stack[T]) | стеки, очереди, деревья, Set[T] | каждое инстанцирование — отдельный тип: Stack[int] и Stack[string] разные |

### Правила гигиены дженериков

- Сигнатура вместо копий: одна func Max[T cmp.Ordered](a, b T) T вместо maxInt/maxString/maxFloat64 — тело правится один раз, компилятор проверяет все типы.
- Минимальный констрейнт: any — только когда тело вообще не оперирует над T; comparable — когда нужны == и !=; union — когда операции известны дословно; Ordered — когда нужен порядок.
- Вывод типов на месте: пишите Max(1, 2) без [int]; явная инстанциация — когда вывод неоднозначен или тип нужен читателю.
- В песочнице нетипизированные дробные константы не выводятся («untyped float does not implement …»): типизируйте переменной fx := 2.5 или инстанцируйте явно Max[float64](2.5, 1.5); локальный go run выводит их честно.
- Обобщённый тип — для контейнеров: type Stack[T any] с методами на приёмнике (s *Stack[T]); помните, что Stack[int] и Stack[string] — разные типы.
- Дженерики не заменяют интерфейсы и any: поведение различается — интерфейсы; данные разнородны — []any и type switch; логика одна, типы разные — только тогда дженерики.

> **Честно о двух движках песочницы.** Урок дженериков — native-only: резервный упрощённый движок не парсит type parameters вовсе, поэтому все интерактивы, кроме первого раннера, исполняются только основным движком (yaegi) и локальным go run. Три разных сообщения об ошибке резервного движка, все проверены пробниками до написания урока. Обобщённая функция — «ожидалось "(", получено "["»: парсер не знает квадратных скобок после имени функции. Union-констрейнт — «ожидался идентификатор, получено "|"»: перечисление типов в интерфейсе не парсится. Обобщённый тип — «ожидалось "]", получено "any"»: type Stack[T any] не парсится. Первый раннер (три копии max) написан без дженериков намеренно — он работает в обоих движках. У основного движка свои честные квирки: нетипизированные ДРОБНЫЕ константы при выводе типов не выводятся — Max(2.5, 1.5) и Sum(1.5, 2.25) отвечают «untyped float does not implement cmp.Ordered» (или main.Number), тогда как настоящий Go выводит T = float64 без слов; обход — типизированные переменные или явная инстанциация. Целые константы выводятся нормально. И второй: golang.org/x/exp/constraints в песочнице не импортируется — «unable to find source related to» — внешний модуль не установлен; cmp.Ordered из stdlib импортируется и работает.

### Что дальше: пакеты и модули

Виток «одна функция — много типов» закрыт полностью. [Одиннадцатый урок](/courses/go-advanced/lessons/11-polimorfizm/) собрал коробку any, [двенадцатый](/courses/go-advanced/lessons/12-type-assertion/) научил её распаковывать — ok-формой, одиночной формой и type switch, — а сегодняшний сделал так, чтобы распаковка была не нужна: тип живёт в сигнатуре, констрейнт обещает операции, компилятор проверяет и тело, и вызовы. Три ответа языка на одну боль — any, assertion, дженерики — теперь у вас все; выбираются они так: поведение — интерфейсы, разнородные данные — any и type switch, одинаковая логика на разных типах — type parameters.

Что дальше? Четырнадцатый урок — пакеты и модули: как код раскладывается по файлам и каталогам, что экспортирует заглавная буква, как работает go mod, зачем внутренние пакеты и как приходят версии. До сих пор все наши программы были одним файлом main.go — пора понять, из чего собирается настоящий проект, и заодно выяснить, почему golang.org/x/exp/constraints из сегодняшнего урока в песочнице не импортируется: у внешних модулей есть собственная дисциплина подключения. Дальше по курсу — ошибки со сквозной упаковкой (errors.Is и errors.As, куда вырастет err.(*MyErr) из двенадцатого урока). Весь список тем — на [странице курса](/courses/go-advanced/).

### Карточки урока

- **Type parameters — что это и где стоят** — Параметры типов в квадратных скобках после имени функции или типа: func Max[T cmp.Ordered](a, b T) T. T — имя тип-параметра, cmp.Ordered — констрейнт (контракт). Конкретный тип подставляется при вызове: неявно выводом типов или явно Max[int](3, 7). Появились в Go 1.18.
- **Констрейнт — зачем и какой минимальный** — Обещание о тип-параметре: тело вправе применять к T только операции констрейнта. any — обещаний нет (только хранение и var zero T); comparable — == и !=; union [T int | float64] — операции перечисленных типов; cmp.Ordered — равенство и порядок. Берите минимальный достаточный: больше типов сможет вызвать функцию.
- **comparable — что обещает и что нет** — Встроенный констрейнт равенства: значения T можно сравнивать == и !=. Подходят числа, строки, булевы, указатели, каналы, интерфейсы, структуры и массивы из сравнимого. Не подходят срезы, карты, функции. Порядка НЕ обещает: a < b с comparable не скомпилируется. Ключи map[T]V обязаны быть comparable.
- **Union-констрейнт — синтаксис и квирк песочницы** — type Number interface { int | float64 } — интерфейс как множество типов, перечисление через |. Тело функции использует операции, общие для множества. Тильда ~float64 включает named-типы поверх float64. В песочнице нетипизированные дробные константы Sum(1.5, 2.25) не выводятся — «untyped float does not implement» — обход: типизированные переменные или Sum[float64](...).
- **Вывод типов — когда нужны явные скобки** — Компилятор сопоставляет типы аргументов с тип-параметрами: Max(1, 2) — T = int без единой скобки. Явная инстанциация Max[int](3, 7) нужна, когда вывод неоднозначен (Max(1, "x") — ошибка), тип не встречается в аргументах или тип хочется назвать читателю. В песочнице дробные нетипизированные константы не выводятся — квирк yaegi, настоящий Go выводит.
- **Обобщённый тип Stack[T any] — устройство** — type Stack[T any] struct { items []T } — тип-параметр на уровне типа; методы получают его в приёмнике: func (s *Stack[T]) Push(v T). Инстанциация Stack[string]{} — конкретный тип; Stack[int] и Stack[string] — разные типы. var zero T — нулевое значение неизвестного типа для Pop на пустом стеке; комма-ок (T, bool) сообщает, был ли элемент.

## Коротко: что запомнить

- Дженерики — type parameters из Go 1.18: одна функция или тип работает со списком типов, и все проверки уходят компилятору — ни any-коробки, ни рантайм-распаковки.
- Сигнатура func Max[T cmp.Ordered](a, b T) T: квадратные скобки — тип-параметры, T — имя, cmp.Ordered — констрейнт; параметры и результат выражены через T.
- До 1.18 жили копиями (maxInt/maxString/maxFloat64) или any + assertion: первый путь платил дублированием, второй — безопасностью и проверкой в рантайме.
- Констрейнт — контракт тип-параметра: any без обещаний, comparable — == и !=, union [T int | float64] — операции множества, cmp.Ordered — равенство и порядок.
- Вывод типов подставляет T из аргументов: Max(1, 2) собирается без [int]; явная инстанциация — при неоднозначности или по желанию читателя.
- cmp.Ordered из stdlib (с Go 1.21) — штатный констрейнт упорядоченности; самодельный union работает тоже; golang.org/x/exp/constraints в песочнице не импортируется.
- Обобщённые типы: type Stack[T any] с методами (s *Stack[T]); каждое инстанцирование — отдельный тип; var zero T — нулевое значение неизвестного типа.
- Дженерики не нужны, когда тип один, когда различается поведение (интерфейсы) и когда данные разнородны ([]any и type switch): они про одинаковую логику на разных типах.

## Домашнее задание

Часть первая — осязайте цену: напишите maxAny(a, b any) any с каскадом ok-форм для int/string/float64 (двенадцатый урок) и сравните с Max[T cmp.Ordered] из раннера: вызовите обе на числах, строках и на смешанной паре; запустите локальным go run и убедитесь, что дженерик ловит смешанную пару на компиляции, а maxAny — только в рантайме. Часть вторая — comparable в деле: напишите Contains[T comparable](s []T, v T) bool линейным поиском и Index[T comparable](s []T, v T) int, проверьте на []string и []int; попробуйте вызвать на []int с float64-аргументом и прочтите ошибку компиляции. Часть третья — union: объявите type Number interface { int | float64 } и функцию Avg[T Number](a, b T) float64 (приведение к float64 внутри — легально, оба типа множества приводимы); проверьте на int и на типизированных float64-переменных; попробуйте Avg(1.5, 2.5) с нетипизированными константами в песочнице и в локальном go run — увидите квирк yaegi из плашки. Часть четвёртая — обобщённый тип: дополните Stack[T any] из раннера методом Len() int и методом Top() (T, bool) без снятия вершины; напишите Set[T comparable] на карте map[T]struct{} с методами Add, Has и Len; соберите go build. Комментарии в коде — по-русски.

## Вопросы и ответы по уроку

### Чем дженерики отличаются от any и type assertion из прошлых уроков?

any прячет тип внутри функции: тело не знает, что лежит в коробке, а распаковка type assertion проверяет динамический тип в рантайме — несовпадение стоит паники interface conversion или флага ok. Дженерик держит тип в сигнатуре: тип-параметр T ограничен констрейнтом, конкретный тип подставляется при вызове, и компилятор проверяет и тело, и каждый вызов на этапе сборки. Распаковка не нужна вовсе: Max(3, 7) возвращает int, а не any.

### Что такое constraint (констрейнт) и какой констрейнт минимальный?

Констрейнт — обещание о тип-параметре, записанное в квадратных скобках: тело функции вправе применять к T только операции, которые констрейнт гарантирует. Минимальный — any: обещаний нет, над T можно только хранить, передавать и брать var zero T. comparable обещает равенство == и !=, union (int | float64) — операции перечисленных типов, cmp.Ordered — равенство и порядок. Правило: минимальный достаточный контракт — чем меньше обещаний, тем шире применимость.

### Почему Max(2.5, 1.5) не собирается в песочнице, а в настоящем Go собирается?

Это квирк основного движка песочницы (yaegi): нетипизированные дробные константы при выводе типов не выводятся, движок отвечает «untyped float does not implement cmp.Ordered», хотя настоящий Go выводит T = float64 без слов. Обход: типизированная переменная (fx, fy := 2.5, 1.5; затем Max(fx, fy)) или явная инстанциация Max[float64](2.5, 1.5). Целые нетипизированные константы выводятся нормально. Локальный go run выводит дробные константы честно.

### Когда дженерики не нужны?

Когда тип на самом деле один — пишите конкретный тип; когда различается поведение, а не тип — это интерфейсы (Stringer, Reader, error); когда данные по-настоящему разнородны (парсинг JSON, плагины) — это []any и type switch; когда телу всё равно, а вызывающему всё равно распаковывать — обычный any. Дженерики про случай «логика одна, типы разные»: функция использует T в сигнатуре дважды и больше, а констрейнт гарантирует нужные операции.
