Generics в Go: зачем нужны и как использовать для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: что такое Generics и почему они появились в Go?

Долгое время сообщество Go (Golang) обходилось без дженериков (обобщённого программирования). Разработчики использовали интерфейсы, пустые interface{} и кодогенерацию, чтобы обойти ограничения строгой типизации. В версии Go 1.18 (февраль 2022 года) в язык наконец-то были добавлены дженерики — мощный инструмент, позволяющий писать функции и структуры, которые работают с разными типами данных без потери безопасности типов.

В этой статье мы разберём, зачем нужны Generics в Go, как они работают, и научимся применять их на практике. Материал рассчитан на начинающих разработчиков, которые уже знакомы с основами Go: переменными, функциями и срезами.

Зачем нужны дженерики в Go?

Представьте, что вам нужно написать функцию, которая находит минимальное значение в срезе чисел. Без дженериков вам пришлось бы писать отдельные функции для int, float64 и других типов:

// Без дженериков — три одинаковые функции
func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

func MinFloat64(a, b float64) float64 {
    if a < b {
        return a
    }
    return b
}

Это приводит к дублированию кода. Раньше эту проблему решали через interface{} и приведение типов, но такой подход терял безопасность на этапе компиляции:

func Min(a, b interface{}) interface{} {
    // Сравнение и приведение — легко ошибиться
    return a
}

Дженерики решают три ключевые задачи:

  • Типобезопасность — ошибки типов отлавливаются на этапе компиляции.
  • Устранение дублирования — одна функция работает с любыми типами.
  • Читаемость кода — явное указание того, какие типы поддерживаются.

Основы синтаксиса: type parameters и constraints

Дженерики в Go строятся на двух концепциях: параметры типа (type parameters) и ограничения (constraints).

Параметры типа

Параметры типа указываются в квадратных скобках после имени функции или типа. По соглашению, чаще всего используют букву T (от Type), но можно использовать любые имена:

// Функция с одним параметром типа T
func Print[T any](value T) {
    fmt.Println(value)
}

// Использование
Print[int](42)      // явно указываем тип
Print("Hello")      // компилятор выводит тип автоматически

Ключевое слово any — это псевдоним для пустого интерфейса interface{}. Оно означает, что функция принимает любой тип.

Ограничения (constraints)

Ограничения определяют, какие типы могут быть использованы с дженериком. Например, для сравнения чисел нужно, чтобы тип поддерживал операторы < и >. Встроенные ограничения для числовых типов находятся в пакете golang.org/x/exp/constraints:

import "golang.org/x/exp/constraints"

// Ограничение — только целые числа
func Sum[T constraints.Integer](numbers []T) T {
    var total T
    for _, n := range numbers {
        total += n
    }
    return total
}

Вы также можете создавать свои ограничения через интерфейсы:

// Собственное ограничение: тип должен иметь метод String()
type Stringer interface {
    String() string
}

func Describe[T Stringer](value T) string {
    return "Value: " + value.String()
}

Практические примеры использования Generics

Рассмотрим несколько реальных сценариев, где дженерики значительно упрощают код.

1. Универсальная функция поиска в срезе

// Находит индекс элемента в срезе. -1 если не найден.
func Index[T comparable](slice []T, target T) int {
    for i, v := range slice {
        if v == target {
            return i
        }
    }
    return -1
}

// Использование
func main() {
    nums := []int{10, 20, 30, 40}
    fmt.Println(Index(nums, 30)) // 2

    words := []string{"go", "rust", "python"}
    fmt.Println(Index(words, "rust")) // 1
}

Обратите внимание на ограничение comparable — оно требует, чтобы тип поддерживал оператор ==. Это все базовые типы (строки, числа, булевы) и структуры, состоящие только из comparable-полей.

2. Generic-структура: стек

// Обобщённый стек для любого типа
type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Использование
func main() {
    intStack := Stack[int]{}
    intStack.Push(10)
    intStack.Push(20)
    val, ok := intStack.Pop()
    fmt.Println(val, ok) // 20 true
}

3. Работа с картами: копирование и преобразование

// Копирует значения из одной карты в другую
func CopyMap[K comparable, V any](src map[K]V) map[K]V {
    dst := make(map[K]V, len(src))
    for k, v := range src {
        dst[k] = v
    }
    return dst
}

// Преобразует все значения через функцию
func MapValues[K comparable, V, R any](m map[K]V, fn func(V) R) map[K]R {
    result := make(map[K]R, len(m))
    for k, v := range m {
        result[k] = fn(v)
    }
    return result
}

// Использование
func main() {
    original := map[string]int{"a": 1, "b": 2}
    copy := CopyMap(original)
    
    doubled := MapValues(original, func(v int) int {
        return v * 2
    })
    fmt.Println(doubled) // map[a:2 b:4]
}

Ограничения и подводные камни

Дженерики в Go — мощный, но

Блоги

Рекомендаций по книгам