Skip to main content
Technology & EngineeringGo239 lines

Generics

Go generics including type parameters, constraints, and generic data structure patterns (Go 1.18+)

Quick Summary30 lines
You are an expert in Go generics (Go 1.18+), helping developers write type-safe, reusable code using type parameters, constraints, and the standard library's generic utilities.

## Key Points

- `any` — alias for `interface{}`; no restriction.
- `comparable` — types that support `==` and `!=`.
- `cmp.Ordered` — types that support `<`, `>`, `<=`, `>=` (Go 1.21+).
- Use generics when you have the same logic for multiple types — do not reach for generics when a concrete type or interface suffices.
- Start with the standard library: `slices`, `maps`, and `cmp` cover most common generic operations.
- Prefer `cmp.Ordered` over writing your own numeric union constraints.
- Keep constraints as narrow as possible — use `comparable` only when you need `==`.
- Use the `~` operator in constraints when you want to accept named types (e.g., `type UserID int`).
- Avoid over-abstracting: a non-generic function with a concrete type is clearer when only one type is used.
- **Cannot use generics with methods (method-level type parameters)**: Go only supports type parameters on functions and types, not on individual methods of a non-generic type.
- **Complex type inference failures**: sometimes Go cannot infer type parameters and you must specify them explicitly: `Filter[int](nums, isEven)`.
- **No generic zero value**: use `var zero T` to get the zero value of a type parameter.

## Quick Example

```go
func Map[T, U any](s []T, f func(T) U) []U
```

```go
type Number interface {
    ~int | ~int64 | ~float64
}
```
skilldb get go-skills/GenericsFull skill: 239 lines
Paste into your CLAUDE.md or agent config

Generics — Go Programming

You are an expert in Go generics (Go 1.18+), helping developers write type-safe, reusable code using type parameters, constraints, and the standard library's generic utilities.

Core Philosophy

Generics in Go were introduced with deliberate restraint. The language waited over a decade to add type parameters, and the result reflects Go's core value: simplicity over expressiveness. Generics exist to eliminate boilerplate duplication where the same algorithm applies identically to multiple types — sorting slices, building sets, writing map/filter/reduce — not to build elaborate type-level abstractions. If you find yourself writing constraints with five type parameters and nested interfaces, you have likely left idiomatic Go territory.

The guiding principle is to reach for generics only when you have concrete, repeated code that differs only in the types involved. A function that works for []int, []string, and []float64 with identical logic is a perfect candidate. A function that works for one type today but "might need to be generic later" is not. Go programmers write concrete code first and generalize only when the duplication becomes real. This keeps code readable for anyone who encounters it — you can understand a concrete function without mentally substituting type variables.

Go's constraint system is intentionally simpler than the type systems in Rust or Haskell. Constraints are interfaces, optionally with type unions, and they describe what operations are available on a type parameter. The standard library's cmp.Ordered, comparable, and any cover the vast majority of use cases. When you need a custom constraint, keep it minimal — the narrower the constraint, the more types can satisfy it, and the more useful your generic code becomes.

Anti-Patterns

  • Premature generalization: Writing a generic function when only one concrete type is used adds cognitive overhead with no benefit. Wait until you have at least two or three concrete duplications before extracting a generic version.

  • Using any as a constraint when a narrower one exists: A function constrained to any cannot do anything useful with its type parameter except store and return it. If you need comparison, use comparable. If you need ordering, use cmp.Ordered. Narrow constraints give you more operations and catch type errors at compile time.

  • Reimplementing standard library generics: The slices, maps, and cmp packages already provide Sort, Contains, Clone, Equal, and many other utilities. Writing your own Contains[T comparable] when slices.Contains exists wastes effort and adds maintenance burden.

  • Deeply nested generic types: Types like Result[Option[Pair[K, V]]] may feel natural coming from Rust or Haskell, but they fight Go's readability culture. Prefer flat, concrete types and use generics at the function level rather than building generic type towers.

  • Using generics to avoid interfaces: Interfaces and generics solve different problems. If you need runtime polymorphism (a handler that accepts any implementation of a contract), use an interface. Generics are for compile-time type parameterization where every instantiation produces the same algorithm with different types.

Overview

Go 1.18 introduced type parameters (generics), allowing functions and types to be parameterized over types. Constraints specify what operations are permitted on type parameters. The constraints package and the comparable built-in provide common constraint building blocks, and Go 1.21+ added the slices, maps, and cmp standard library packages built on generics.

Core Concepts

Type Parameters

Declared in square brackets before the regular parameter list.

func Map[T, U any](s []T, f func(T) U) []U

Constraints

Interfaces that restrict which types can be used as type arguments.

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

~ (Tilde) Operator

Matches the underlying type, so ~int accepts any named type with int as its underlying type.

Built-in Constraints

  • any — alias for interface{}; no restriction.
  • comparable — types that support == and !=.
  • cmp.Ordered — types that support <, >, <=, >= (Go 1.21+).

Implementation Patterns

Generic Functions

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

func Reduce[T, U any](s []T, init U, fn func(U, T) U) U {
    acc := init
    for _, v := range s {
        acc = fn(acc, v)
    }
    return acc
}

func Contains[T comparable](s []T, target T) bool {
    for _, v := range s {
        if v == target {
            return true
        }
    }
    return false
}

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

Custom Constraints

type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64
}

func Sum[T Numeric](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Constraint with methods
type Stringer interface {
    comparable
    String() string
}

Generic Data Structures

// Generic Set
type Set[T comparable] struct {
    items map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{items: make(map[T]struct{})}
}

func (s *Set[T]) Add(v T)           { s.items[v] = struct{}{} }
func (s *Set[T]) Contains(v T) bool { _, ok := s.items[v]; return ok }
func (s *Set[T]) Remove(v T)        { delete(s.items, v) }
func (s *Set[T]) Len() int          { return len(s.items) }

func (s *Set[T]) Values() []T {
    vals := make([]T, 0, len(s.items))
    for v := range s.items {
        vals = append(vals, v)
    }
    return vals
}
// Generic Result type
type Result[T any] struct {
    Value T
    Err   error
}

func Ok[T any](v T) Result[T]    { return Result[T]{Value: v} }
func Fail[T any](err error) Result[T] { return Result[T]{Err: err} }

func (r Result[T]) Unwrap() (T, error) { return r.Value, r.Err }

Generic Repository Pattern

type Entity interface {
    comparable
    GetID() string
}

type Repository[T Entity] struct {
    store map[string]T
    mu    sync.RWMutex
}

func NewRepository[T Entity]() *Repository[T] {
    return &Repository[T]{store: make(map[string]T)}
}

func (r *Repository[T]) Get(id string) (T, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    v, ok := r.store[id]
    return v, ok
}

func (r *Repository[T]) Save(entity T) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.store[entity.GetID()] = entity
}

Using Standard Library Generics (Go 1.21+)

import (
    "cmp"
    "slices"
    "maps"
)

// Sort a slice
slices.Sort(nums)
slices.SortFunc(users, func(a, b User) int {
    return cmp.Compare(a.Name, b.Name)
})

// Search
idx, found := slices.BinarySearch(sorted, target)

// Map operations
keys := maps.Keys(m)
maps.DeleteFunc(m, func(k string, v int) bool {
    return v == 0
})

// Clone
clone := maps.Clone(original)

Best Practices

  • Use generics when you have the same logic for multiple types — do not reach for generics when a concrete type or interface suffices.
  • Start with the standard library: slices, maps, and cmp cover most common generic operations.
  • Prefer cmp.Ordered over writing your own numeric union constraints.
  • Keep constraints as narrow as possible — use comparable only when you need ==.
  • Use the ~ operator in constraints when you want to accept named types (e.g., type UserID int).
  • Avoid over-abstracting: a non-generic function with a concrete type is clearer when only one type is used.

Common Pitfalls

  • Cannot use generics with methods (method-level type parameters): Go only supports type parameters on functions and types, not on individual methods of a non-generic type.
  • Complex type inference failures: sometimes Go cannot infer type parameters and you must specify them explicitly: Filter[int](nums, isEven).
  • No generic zero value: use var zero T to get the zero value of a type parameter.
  • Pointer receiver constraints: to require a pointer method, use interface { *T; Method() } patterns or pass *T explicitly.
  • Union constraints are not sum types: you cannot type-switch on a union constraint; you can only use the operations the constraint allows.
  • Performance: generic functions may be slower than monomorphized code in micro-benchmarks due to dictionary-based dispatch; measure before worrying.

Install this skill directly: skilldb add go-skills

Get CLI access →