Generics
Go generics including type parameters, constraints, and generic data structure patterns (Go 1.18+)
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 linesGenerics — 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
anyas a constraint when a narrower one exists: A function constrained toanycannot do anything useful with its type parameter except store and return it. If you need comparison, usecomparable. If you need ordering, usecmp.Ordered. Narrow constraints give you more operations and catch type errors at compile time. -
Reimplementing standard library generics: The
slices,maps, andcmppackages already provideSort,Contains,Clone,Equal, and many other utilities. Writing your ownContains[T comparable]whenslices.Containsexists 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 forinterface{}; 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, andcmpcover most common generic operations. - Prefer
cmp.Orderedover writing your own numeric union constraints. - Keep constraints as narrow as possible — use
comparableonly 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 Tto get the zero value of a type parameter. - Pointer receiver constraints: to require a pointer method, use
interface { *T; Method() }patterns or pass*Texplicitly. - 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
Related Skills
Context Patterns
Context usage for cancellation, timeouts, deadlines, and value propagation in Go
Error Handling
Error handling patterns including wrapping, sentinel errors, custom types, and error groups in Go
Goroutines Channels
Concurrency patterns using goroutines, channels, select, and sync primitives in Go
HTTP Servers
HTTP server patterns using net/http, chi, and gin including middleware, routing, and graceful shutdown
Interfaces
Interface design principles, implicit satisfaction, and composition patterns in Go
Modules
Go modules, dependency management, versioning, workspaces, and private module configuration