Go generics на практике: паттерны, ограничения и реальные примеры
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 намеренный, и в большинстве случаев его достаточно.