Расскажите о дженериках в Go: когда они появились, как их использовать и какие есть ограничения?

Ответ

Да, конечно. Дженерики (generics), или обобщённое программирование, появились в Go версии 1.18. Они позволяют писать функции и структуры данных, которые могут работать с любым типом, удовлетворяющим определённым ограничениям (constraints).

Основные концепции:

  1. Параметры типа (Type Parameters): Объявляются в квадратных скобках после имени функции или типа. [T any]
  2. Ограничения (Constraints): Это интерфейсы, которые определяют, какие операции должен поддерживать тип, чтобы его можно было использовать в качестве аргумента типа. В Go есть два встроенных ограничения:
    • any: псевдоним для interface{}, разрешает любой тип.
    • comparable: разрешает типы, которые поддерживают операции сравнения == и !=. Также можно определять свои собственные ограничения: type Number interface { int | float64 }.

Примеры использования:

  • Обобщённые функции:

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

    // Stack - простая реализация стека для любого типа.
    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 // Возвращаем zero-value для типа T
            return zero, false
        }
        item := s.items[len(s.items)-1]
        s.items = s.items[:len(s.items)-1]
        return item, true
    }

Ограничения и особенности:

  • Нет специализации: В отличие от C++, в Go нельзя предоставить отдельную реализацию обобщённой функции для конкретного типа.
  • Ограничения на методы: Нельзя объявлять методы с параметрами типа. То есть, дженериками могут быть функции и типы, но не методы.
  • Производительность: Компилятор Go создаёт отдельную реализацию функции для каждого конкретного типа, используемого с дженериком. Это обеспечивает высокую производительность во время выполнения (runtime), но может увеличить размер бинарного файла.

Я использую дженерики в основном для написания утилитарных функций (работа с коллекциями, как в примере Map) и для создания типобезопасных структур данных (стеки, очереди, деревья). Это помогает избежать дублирования кода и ошибок, связанных с приведением типов interface{}.