Generics в Go: зачем нужны и как использовать на практике

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

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

Начать курс

Введение: Почему Generics в Go — это прорыв?

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

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

Зачем нужны Generics в Go?

До появления дженериков для создания универсальных функций разработчики использовали interface{} (пустой интерфейс). Рассмотрим классический пример — функция, которая находит минимальный элемент в слайсе:

// Без дженериков (Go < 1.18)
func MinInt(a, b int) int {
    if a < b {
        return a
    }
    return b
}

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

Для каждого типа приходилось писать отдельную функцию. С interface{} можно было сделать так:

func MinInterface(a, b interface{}) interface{} {
    // Требуется приведение типов и проверка на этапе выполнения
    // Это небезопасно и неэффективно
}

Проблемы такого подхода очевидны:

  • Потеря типовой безопасности — ошибка может проявиться только во время выполнения.
  • Необходимость приведения типов — код становится громоздким.
  • Снижение производительности — работа с интерфейсами требует дополнительных операций.

Generics решают все эти проблемы. Они позволяют:

  • Писать универсальные алгоритмы один раз.
  • Сохранять строгую проверку типов на этапе компиляции.
  • Избавиться от приведения типов и повысить производительность.

Основы синтаксиса Generics в Go

Дженерики в Go основаны на параметрах типа. Синтаксис прост и интуитивно понятен. Параметры типа указываются в квадратных скобках после имени функции или типа.

1. Простая функция с дженериками

Напишем ту же функцию минимума, но теперь с использованием дженериков:

package main

import "fmt"

// Min возвращает минимальное значение из двух
func Min[T comparable](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min[int](10, 20))       // 10
    fmt.Println(Min[float64](3.14, 2.71)) // 2.71
    fmt.Println(Min[string]("abc", "xyz")) // abc
}

Здесь [T comparable] — это параметр типа. comparable — это встроенное ограничение (constraint), которое гарантирует, что тип поддерживает операторы сравнения == и !=. Обратите внимание: для оператора < нужно использовать ограничение constraints.Ordered (из пакета golang.org/x/exp/constraints) или написать своё.

2. Ограничения (Constraints)

Ограничения — это интерфейсы, которые определяют, какие методы и операции должны поддерживать типы. В Go 1.18+ появился пакет constraints (пока находится в golang.org/x/exp), но вы можете создавать свои ограничения.

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

// MinOrdered работает только с упорядоченными типами
func MinOrdered[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

Собственное ограничение для типов, поддерживающих метод String():

type Stringer interface {
    String() string
}

// PrintStrings печатает строковое представление
func PrintStrings[T Stringer](items []T) {
    for _, item := range items {
        fmt.Println(item.String())
    }
}

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

Теперь перейдём к реальным задачам, которые часто встречаются в Go-разработке.

1. Обобщённая структура данных: стек

Создадим типобезопасный стек, который может работать с любым типом:

package main

import "fmt"

// Stack — обобщённая структура стека
type Stack[T any] struct {
    items []T
}

// Push добавляет элемент в стек
func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

// Pop удаляет и возвращает верхний элемент
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(1)
    intStack.Push(2)
    fmt.Println(intStack.Pop()) // 2 true

    stringStack := Stack[string]{}
    stringStack.Push("hello")
    stringStack.Push("world")
    fmt.Println(stringStack.Pop()) // world true
}

Обратите внимание на [T any] — any это псевдоним для interface{}, который позволяет использовать любой тип без ограничений.

2. Утилитарные функции: Map и Filter

Функции высшего порядка, знакомые по другим языкам, теперь можно реализовать типобезопасно:

// Map применяет функцию f к каждому элементу слайса
func Map[T, U any](items []T, f func(T) U) []U {
    result := make([]U, len(items))
    for i, item := range items {
        result[i] = f(item)
    }
    return result
}

// Filter оставляет элементы, удовлетворяющие предикату
func Filter[T any](items []T, pred func(T) bool) []T {
    var result []T
    for _, item := range items {
        if pred(item) {
            result = append(result, item)
        }
    }
    return result
}

func main() {
    numbers := []int                

Блоги

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