---
title: "Введение в дженерики"
description: "Представьте, что вам нужна функция «найти максимум из двух значений». Для int вы её пишете. Потом приходит задача с float64 — и вы пишете почти такую же, отличается только тип…"
canonical: https://graphlms.ru/go-basics/book/generics
course: "Основы Go"
updated: 2026-10-12
lang: ru
publisher: GraphLMS
---

# Введение в дженерики

> Источник: https://graphlms.ru/go-basics/book/generics — курс «Основы Go», GraphLMS.

## О чём глава

Представьте, что вам нужна функция «найти максимум из двух значений». Для `int` вы её пишете. Потом приходит задача с `float64` — и вы пишете почти такую же, отличается только тип. Потом `string` — и снова копипаста. Тело функции одно и то же, меняется лишь тип данных, но компилятор Go до версии 1.18 не давал выразить это одним куском кода.

Дженерики (обобщённое программирование) позволяют написать алгоритм **один раз**, оставив тип «пустым местом», которое компилятор заполнит при вызове. В отличие от приёма с `any` из [главы об интерфейсах](/go-basics/book/interfaces), проверки типов при этом сохраняются: ошибка обнаружится при сборке, а не во время работы программы.

Глава опирается на функции, срезы и интерфейсы. Самим писать обобщённый код в начале карьеры приходится нечасто, а вот читать его — постоянно: обобщёнными сделаны пакеты `slices` и `maps` стандартной библиотеки. Поэтому главная цель — научиться читать сигнатуры вроде `func Contains[T comparable](xs []T, target T) bool`. В курсе по конкурентности дженерики не понадобятся, и упражнение этой главы можно отложить.

## Зачем вообще: три подхода к «максимуму»

Зайдём от проблемы. Допустим, нам нужен максимум из двух чисел. Есть три исторических способа, и у первых двух — заметные минусы.

**Способ 1: дублирование.** Пишем отдельную функцию под каждый тип.

```go
func MaxInt(a, b int) int {
    if a > b {
        return a
    }
    return b
}

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

Тело идентично, а функций две. Добавится новый тип — будет три. Это шумно и легко рассинхронизировать.

**Способ 2: `any` (пустой интерфейс).** Принимаем «что угодно», но тогда теряем типы: внутри нельзя написать `a > b`, придётся доставать значения через type assertion, а компилятор перестаёт нас страховать. Передадите по ошибке строку и число — узнаете об этом только во время работы программы.

**Способ 3: дженерики.** Описываем функцию с параметром типа `T`, и компилятор сам подставляет конкретный тип в месте вызова, сохраняя все проверки. Тело одно, и типы проверяются, как в первом способе.

## Параметры типа: `[T any]`

Синтаксис дженериков добавляет к функции **список параметров типа** — в квадратных скобках, сразу после имени, перед обычными аргументами.

```go
func Identity[T any](v T) T {
    return v
}
```

Разберём по частям:

- `[T any]` — мы вводим параметр типа с именем `T`. Имя любое (часто берут `T`, `K`, `V`, `E`), но по традиции — одна заглавная буква.
- `any` — это **ограничение**: какие типы разрешено подставлять вместо `T`. `any` означает «любой» — это тот же пустой интерфейс из главы об интерфейсах.
- Дальше `T` используется как обычный тип: и в аргументе `v T`, и в типе результата `T`.

Вызвать можно явно указав тип в скобках — `Identity[int](5)` — но чаще тип **выводится** из аргумента автоматически:

```go play
package main

import "fmt"

func Identity[T any](v T) T {
	return v
}

func main() {
	fmt.Println(Identity(42))      // T выведен как int
	fmt.Println(Identity("привет")) // T выведен как string
	fmt.Println(Identity(3.14))     // T выведен как float64
}
```

Одна функция — три разных типа, и ни одного приведения. Компилятор сам понял, что подставить вместо `T`, по типу переданного значения.

## Ограничения (constraints): что разрешено типу

`any` говорит «подойдёт любой тип». Но часто этого мало. Вернёмся к `Max`: внутри нам нужно сравнить `a > b`. А оператор `>` определён не для всех типов — например, две структуры так сравнивать нельзя. Значит, надо сообщить компилятору: «`T` — не любой, а только такой, который умеет в `>`».

Это и есть **ограничение** (constraint). Технически ограничение — это интерфейс, но не совсем обычный: он описывает не методы, а **множество допустимых типов**. Самый прямой способ — перечислить эти типы.

### Своё ограничение через интерфейс с типами

Объявим интерфейс, который перечисляет, какие типы мы разрешаем. Это полноценный список «или то, или то»:

```go
type Ordered interface {
	int | int64 | float64 | string
}
```

Читается как «`Ordered` — это `int`, либо `int64`, либо `float64`, либо `string`». Для всех этих типов оператор `>` имеет смысл, поэтому внутри функции мы вправе им пользоваться. Теперь напишем `Max`:

```go play
package main

import "fmt"

type Ordered interface {
	int | int64 | float64 | string
}

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

func main() {
	fmt.Println(Max(3, 9)) // T = int

	fmt.Println(Max(2.5, 1.5)) // T = float64

	fmt.Println(Max("яблоко", "груша")) // T = string
}
```

Вывод:

```text
9
2.5
яблоко
```

Один алгоритм отработал для трёх разных типов, и компилятор на каждом вызове проверил, что тип действительно входит в `Ordered`. Попробуйте вызвать `Max(true, false)`: сборка упадёт, потому что `bool` в списке нет. Строки сравниваются побайтово; для русских букв это совпадает с алфавитным порядком (кроме «ё»), поэтому «яблоко» больше «груши».

Писать `Ordered` руками в рабочем коде не нужно. С Go 1.21 в стандартной библиотеке есть готовое ограничение `cmp.Ordered` (пакет `cmp`), а для двух чисел — встроенные функции `max` и `min`. Мы объявили ограничение сами, чтобы было видно, как оно устроено.

### `comparable`: типы, которые можно сравнивать на равенство

Есть встроенное ограничение `comparable` — оно разрешает типы, которые поддерживают операторы `==` и `!=`. Это не то же самое, что `>`/`<`: на равенство можно сравнивать гораздо больше типов (числа, строки, булевы, указатели), а вот «больше/меньше» — нет.

`comparable` нужен, например, чтобы тип можно было использовать как ключ `map` или искать в срезе. Вот функция «есть ли элемент в срезе»:

```go play
package main

import "fmt"

func Contains[T comparable](xs []T, target T) bool {
	for _, x := range xs {
		if x == target {
			return true
		}
	}
	return false
}

func main() {
	nums := []int{2, 4, 6, 8}
	fmt.Println(Contains(nums, 6)) // true
	fmt.Println(Contains(nums, 5)) // false

	words := []string{"go", "rust", "zig"}
	fmt.Println(Contains(words, "rust")) // true
}
```

Внутри мы пользуемся `==`, и `comparable` гарантирует, что для любого подставленного `T` это корректно. Передать сюда тип, который сравнивать на равенство нельзя (скажем, срез), компилятор просто не позволит.

> Есть тонкость: формально под `comparable` подходят и интерфейсные типы, но если внутри интерфейса окажется несравнимое значение (например, срез), сравнение `==` может вызвать панику уже в рантайме. На практике для джуна это редкий случай — детальный разбор выходит за рамки главы.

### `~` — «этот тип и все на его основе»

Есть нюанс. В Go можно объявить **свой тип на базе существующего**:

```go
type Age int
```

`Age` — отдельный тип, хотя устроен он как `int`. И вот загвоздка: если ограничение перечисляет ровно `int`, то `Age` под него **не подойдёт**, ведь формально это уже не `int`, а `Age`.

Чтобы охватить и базовый тип, и всех его «потомков», в ограничении ставят тильду `~`:

```go
type Number interface {
	~int | ~float64
}
```

`~int` читается как «`int` и любой тип, в основе которого лежит `int`». Теперь под `Number` подойдёт и `int`, и наш `Age`:

```go play
package main

import "fmt"

type Number interface {
	~int | ~float64
}

type Age int

func Sum[T Number](xs []T) T {
	var total T // нулевое значение T: 0 для любого числа
	for _, x := range xs {
		total += x
	}
	return total
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2}))
	fmt.Println(Sum([]Age{20, 22}))
}
```

Вывод:

```text
6
3.5
42
```

Уберите тильду у `~int` и запустите снова: третий вызов перестанет собираться, потому что `Age` — не `int`. На практике `~` ставят почти всегда, чтобы ограничение работало и с пользовательскими типами.

## Дженерик-типы (кратко)

Параметры типа бывают не только у функций, но и у **типов** — структур, например. Это позволяет описать контейнер, который хранит элементы какого угодно типа, но при этом остаётся типобезопасным: «стопка строк» и «стопка чисел» — разные типы, и перепутать их нельзя.

```go play
package main

import "fmt"

// Stack — стопка элементов типа T (last in, first out).
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 // пусто — отдаём нулевое значение T
	}
	last := s.items[len(s.items)-1]
	s.items = s.items[:len(s.items)-1]
	return last, true
}

func main() {
	var s Stack[string] // стопка именно строк
	s.Push("a")
	s.Push("b")
	v, ok := s.Pop()
	fmt.Println(v, ok) // b true
}
```

Несколько важных деталей:

- `Stack[T any]` — параметр типа объявляется у самого типа, в его заголовке.
- В методах получатель пишется как `*Stack[T]` — мы «протаскиваем» тот же `T` внутрь методов.
- `var zero T` — частый приём: так получают **нулевое значение** параметра типа, не зная заранее, что это (для чисел — `0`, для строк — `""`, для указателей — `nil`). Удобно возвращать из методов, когда «ничего нет».
- `Stack[string]` при объявлении переменной фиксирует `T = string`. Положить в такую стопку число компилятор не даст.

Самим писать обобщённые типы на старте приходится редко, а **пользоваться** обобщёнными функциями вы будете постоянно: `slices.Contains`, `slices.Sort`, `slices.Index`, `maps.Keys` из стандартной библиотеки устроены именно так. Они подключаются обычным `import "slices"` и `import "maps"`, в примерах с кнопкой тоже.

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

Дженериками легко увлечься. Простое правило: если у вас **один-два** конкретных типа и дублирование невелико — часто понятнее оставить как есть. Дженерики окупаются, когда:

- вы пишете **структуру данных** или утилиту, которой реально всё равно на тип (контейнеры, `Map`/`Filter`/`Reduce` по срезу, кэш);
- иначе пришлось бы копировать одно и то же тело под много типов.

А вот ради единственного вызова городить параметры типа не стоит — это усложняет чтение без выгоды. «Сначала напиши конкретно, обобщи, когда дублирование станет реальным» — здоровая стратегия.

## Частые ошибки

- **Берут `any`, когда нужно сравнение.** Под `any` нельзя писать `a > b` или `a == b` — компилятор не знает, что тип это умеет. Нужно сравнивать на порядок — задайте ограничение со списком типов (как `Ordered`); на равенство — возьмите `comparable`.
- **Забывают `~` в ограничении.** Если перечислить `int` без тильды, пользовательский тип `type ID int` под ограничение не подойдёт. В большинстве случаев пишите `~int`, `~string` и т.д.
- **Путают `comparable` и «упорядочиваемость».** `comparable` — это только `==`/`!=`, но **не** `<`/`>`. Для «больше/меньше» нужен явный список типов, поддерживающих порядок.
- **Тащат дженерики туда, где хватит обычной функции.** Если тип ровно один — обобщение лишь добавляет шума. Обобщайте по факту дублирования, а не на всякий случай.

## Попробуйте сами

Напишите `First[T any](xs []T) (T, bool)`: для непустого среза она возвращает первый элемент и `true`, для пустого — нулевое значение типа `T` и `false`. Проверьте `[]int{4, 9}`, `[]string{"Go"}` и `[]string{}`. Объясните, почему ограничение `comparable` не требуется.

### Проверка

Результаты — `4 true`, `Go true` и пустая строка с `false`:

```go
func First[T any](xs []T) (T, bool) {
    if len(xs) == 0 {
        var zero T
        return zero, false
    }
    return xs[0], true
}
```

Здесь нет сравнения элементов друг с другом, поэтому подходит `any`. Проверка длины защищает от выхода за границы на `xs[0]`. Чтобы пустая строка была видна в выводе, печатайте результат через `%q`.

Обратите внимание на вызов с пустым срезом: `First([]string{})` работает, а `First(nil)` не соберётся. Из `nil` компилятор не может понять, что подставить вместо `T`, и попросит указать тип явно: `First[string](nil)`.

## Что дальше

Пока весь наш код помещался в один файл. Дальше — [«Пакеты и модули»](/go-basics/book/packages-modules): разложим код по нескольким файлам и пакетам и напишем первый тест. Понадобится локальный Go и учебная папка из [первой главы](/go-basics/book/hello).
