Какие средства обобщенного программирования есть в Go?go-102

В отличие от языков с шаблонами (C++, Rust) или классическими дженериками (Java, C#), Go долгое время обходился без них, но сейчас предлагает несколько подходов для обобщенного программирования:

1. Дженерики - с Go 1.18

Наиболее современный и типобезопасный способ:

// Обобщенная функция
func Map[T any, U any](slice []T, mapper func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = mapper(v)
    }
    return result
}

// Обобщенный тип
type Stack[T any] struct {
    items []T
}

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

Особенности:

  • Работают во время компиляции (zero-cost в runtime)
  • Поддерживают constraints (ограничения типов)
  • Полная типобезопасность

2. Интерфейсы

Классический Go-way до появления дженериков:

type Sorter interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

func Sort(s Sorter) {
    // реализация сортировки
}

Плюсы:

  • Работает во всех версиях Go
  • Хорошо подходит для поведения (behavior)

Минусы:

  • Нет compile-time проверок
  • Требует приведения типов (type assertion)

3. Code Generation

Использование go:generate для создания типоспецифичного кода:

//go:generate genny -in=generic_stack.go -out=int_stack.go -pkg=main gen "ItemType=int"

Популярные инструменты:

  • genny (от проекта GoKit)
  • stringer (из stdlib)
  • mockgen

4. Пустой интерфейс

Наименее типобезопасный, но универсальный способ:

func Print(v interface{}) {
    switch val := v.(type) {
    case int:
        fmt.Println("int:", val)
    case string:
        fmt.Println("string:", val)
    }
}

Когда использовать:

  • Когда тип действительно неизвестен (например, JSON)
  • В legacy-коде до Go 1.18

5. Reflection

Для сложных случаев динамического поведения:

func GetField(obj interface{}, field string) interface{} {
    v := reflect.ValueOf(obj)
    return v.FieldByName(field).Interface()
}

Осторожно:

  • Медленнее в 50-100 раз чем дженерики
  • Нет compile-time проверок
  • Сложный код

Сравнение подходов

Метод Типобезопасность Производительность Читаемость Гибкость
Дженерики ✅ Полная ⚡ Максимальная 👍 Средняя
Интерфейсы ❌ Runtime 🐢 Средняя 👍 Низкая
Генерация кода ✅ Полная ⚡ Максимальная 👎 Высокая
interface{} ❌ Нет 🐢 Низкая 👎 Макс.
Reflection ❌ Runtime 🐌 Очень низкая 👎 Макс.

Резюмируем

В современном Go (1.18+) дженерики — основной инструмент для обобщенного программирования. Для legacy-кода или особых случаев можно использовать интерфейсы или code generation. Reflection и interface{} следует применять только когда другие подходы невозможны.