lenec ru

← все посты

Go generics на практике: паттерны, ограничения и реальные примеры

17K

Generics появились в Go 1.18 и за два года стали частью повседневного кода. Но многие до сих пор используют их только для тривиальных случаев или избегают вовсе. Разберём синтаксис, полезные паттерны, ограничения и реальный пример — generic repository для работы с базой данных.

Синтаксис: type parameters и constraints

Базовая generic-функция принимает type parameter в квадратных скобках:

func Map[T any, R any](slice []T, fn func(T) R) []R {
    result := make([]R, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Использование
names := Map(users, func(u User) string { return u.Name })

Constraint any — это алиас для interface{}, принимает любой тип. Для более строгих ограничений используются интерфейсы:

type Number interface {
    ~int | ~int64 | ~float64
}

func Sum[T Number](values []T) T {
    var total T
    for _, v := range values {
        total += v
    }
    return total
}

Оператор ~ означает «underlying type» — constraint принимает не только int, но и любой тип, определённый как type MyInt int.

Встроенные constraints

Стандартная библиотека предоставляет пакет cmp (Go 1.21+) и golang.org/x/exp/constraints:

  • comparable — встроенный constraint, поддерживает == и != (нужен для map keys)
  • cmp.Ordered — типы с операторами < > <= >= (числа и строки)
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

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

Полезные паттерны

Filter/Reduce:

func Filter[T any](slice []T, predicate func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if predicate(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T any, R any](slice []T, initial R, fn func(R, T) R) R {
    acc := initial
    for _, v := range slice {
        acc = fn(acc, v)
    }
    return acc
}

Result type — альтернатива паре (value, error) для цепочек:

type Result[T any] struct {
    Value T
    Err   error
}

func NewResult[T any](val T, err error) Result[T] {
    return Result[T]{Value: val, Err: err}
}

func (r Result[T]) Map(fn func(T) T) Result[T] {
    if r.Err != nil {
        return r
    }
    return Result[T]{Value: fn(r.Value)}
}

func (r Result[T]) AndThen(fn func(T) Result[T]) Result[T] {
    if r.Err != nil {
        return r
    }
    return fn(r.Value)
}

Когда generics нужны, а когда нет

Используйте generics когда:

  • Функция работает с коллекциями произвольных типов (Map, Filter, Sort)
  • Нужен type-safe контейнер (Stack, Queue, Set, SyncMap)
  • Паттерн повторяется для разных типов с одинаковой логикой

Не используйте когда:

  • Достаточно конкретного интерфейса — io.Reader лучше чем T any
  • Один-два типа — проще написать две функции, чем generic
  • Нужен полиморфизм поведения — интерфейсы с методами подходят лучше

Ограничения Go generics

Go generics намеренно минималистичны. Чего нет:

  • Method type parameters — нельзя добавить type parameter к методу (только к типу или функции)
  • Specialization — нельзя дать особую реализацию для конкретного типа
  • Variadic type parameters — нельзя написать Tuple[T...]
  • Constraint на поля структуры — нельзя потребовать «тип с полем Name string»
// Это НЕ компилируется — method type params запрещены
type Repo struct{}
func (r Repo) Find[T any](id string) (T, error) { ... }

// Решение — type parameter на уровне типа
type Repo[T any] struct{}
func (r Repo[T]) Find(id string) (T, error) { ... }

Реальный пример: generic repository

CRUD-репозиторий для любой сущности с базой данных:

type Entity interface {
    TableName() string
}

type Repository[T Entity] struct {
    db *sql.DB
}

func NewRepository[T Entity](db *sql.DB) *Repository[T] {
    return &Repository[T]{db: db}
}

func (r *Repository[T]) FindByID(ctx context.Context, id string) (T, error) {
    var entity T
    table := entity.TableName()
    query := fmt.Sprintf("SELECT * FROM %s WHERE id = $1", table)
    row := r.db.QueryRowContext(ctx, query, id)
    err := row.Scan(&entity) // упрощённо
    return entity, err
}

func (r *Repository[T]) FindAll(ctx context.Context, limit, offset int) ([]T, error) {
    var entity T
    table := entity.TableName()
    query := fmt.Sprintf("SELECT * FROM %s LIMIT $1 OFFSET $2", table)
    rows, err := r.db.QueryContext(ctx, query, limit, offset)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var results []T
    for rows.Next() {
        var item T
        if err := rows.Scan(&item); err != nil {
            return nil, err
        }
        results = append(results, item)
    }
    return results, rows.Err()
}

Использование:

type User struct {
    ID    string
    Name  string
    Email string
}

func (u User) TableName() string { return "users" }

userRepo := NewRepository[User](db)
user, err := userRepo.FindByID(ctx, "abc-123")
users, err := userRepo.FindAll(ctx, 20, 0)

Один тип Repository[T] работает для User, Order, Product — без дублирования кода и без потери типизации. Компилятор проверяет типы на этапе сборки, а не в рантайме.

Итог

Go generics — не замена интерфейсам, а дополнение. Используйте их для коллекций, контейнеров и повторяющихся паттернов. Не пытайтесь превратить Go в Haskell — минимализм generics в Go намеренный, и в большинстве случаев его достаточно.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.