Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~24 мин +50 XP

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

Дженерики 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] и случаи, когда дженерики не нужны. Все примеры запускаются прямо на странице — в основном движке песочницы; где резервный движок пасует, честно сказано в тексте и в плашке урока.

Содержание урока · 14 разделов

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

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

У нас уже есть три инструмента для лозунга «одна функция — много типов», и каждый платит свою цену. Интерфейсы дал десятый урок, коробку any — одиннадцатый, инструменты распаковки — двенадцатый. Интерфейсы дают полиморфизм поведения: любой тип с нужными методами подходит, но тело функции видит только методы контракта. 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 внутри — это коробка, из которой арифметику не достать (одиннадцатый урок показал это на 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, которые резервный движок не парсит вовсе.

Вывод

Запустите код, чтобы увидеть вывод.

Анатомия 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 работают честно.

Вывод

Запустите код, чтобы увидеть вывод.

Вывод типов: 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?

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

Констрейнт — это обещание о тип-параметре, записанное в сигнатуре. Тело функции вправе применять к значениям 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 } — какой констрейнт должен стоять вместо вопроса, чтобы функция собралась?

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

Вывод

Запустите код, чтобы увидеть вывод.

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 на числах и строках?

Прочитайте код и выберите, что он выведет:

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))
}

Обобщённые типы: 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.

Вывод

Запустите код, чтобы увидеть вывод.

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

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

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)) }

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

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

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

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

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

Случай второй: различается поведение, а не тип. Если «разные типы» на самом деле означают «разные действия» — это территория интерфейсов из одиннадцатого урока: контракт поведения, 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; логика одна, типы разные — только тогда дженерики.

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

Виток «одна функция — много типов» закрыт полностью. Одиннадцатый урок собрал коробку any, двенадцатый научил её распаковывать — 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) из двенадцатого урока). Весь список тем — на странице курса.

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

Термин

Type parameters — что это и где стоят

нажмите, чтобы перевернуть
Определение

Параметры типов в квадратных скобках после имени функции или типа: func Max[T cmp.Ordered](a, b T) T. T — имя тип-параметра, cmp.Ordered — констрейнт (контракт). Конкретный тип подставляется при вызове: неявно выводом типов или явно Max[int](3, 7). Появились в Go 1.18.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Дженерики — 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. Комментарии в коде — по-русски.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Pro · Дженерики: обобщённые функции и типы)

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

Чем дженерики отличаются от 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 в сигнатуре дважды и больше, а констрейнт гарантирует нужные операции.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Fiber25 уроков~13 чпрактика25 уроков уже доступны

Fiber: экспресс-подобный веб-фреймворк на Fasthttp

Курс Fiber для Go: маршруты, middleware, JSON, CORS, WebSocket, шаблоны, лимитеры и тестирование. 25 уроков с примерами кода и итоговым REST API.

  • Поднимать HTTP-сервер на Fiber за минуты
  • Проектировать маршруты, группы и версии API
  • Писать middleware: логи, CORS, лимиты, recovery
  • Принимать и отдавать JSON с валидацией