| name | go-generics |
| description | Use when deciding whether to introduce Go generics, writing generic functions or types, composing type constraints, or choosing between type aliases and type definitions. Apply proactively when a user is writing a utility function that could conceivably work with multiple types, even if they didn't mention generics. Does not cover interface-only designs (see go-interfaces). |
| user-invocable | false |
| license | MIT |
| compatibility | Designed for Claude Code or similar AI coding agents. Generics require Go 1.18+; `cmp.Ordered` requires Go 1.21+. |
| metadata | {"author":"muratmirgun","version":"0.1.0","openclaw":{"emoji":"🧬","homepage":"https://github.com/muratmirgun/gophers","requires":{"bins":["go"]},"install":[]}} |
| allowed-tools | Read Edit Write Glob Grep Bash(go:*) Bash(golangci-lint:*) |
Go Generics
Generics are a powerful but easy-to-misuse feature. The Go answer is pragmatic: write concrete code first, then generalize only when you have a real second caller.
Core Rules
- Write concrete first. Reach for generics only when a second type actually needs the same logic.
- If an interface already models the behavior, use the interface. Don't pile type parameters on top.
- Prefer standard constraints (
comparable, cmp.Ordered, any) over hand-rolled unions.
- Don't over-constrain.
comparable is usually enough; the narrower the constraint, the fewer callers benefit.
- Name type parameters with a single uppercase letter (
T, K, V, E) unless a longer name genuinely helps.
- Don't use generics for interface satisfaction.
func F[T io.Reader](r T) is just func F(r io.Reader).
- Don't wrap stdlib containers "for generic convenience" unless you eliminate real duplication.
Decision Flow
Multiple types need the same logic?
├─ No → concrete type
├─ Yes → do they share a useful interface?
│ ├─ Yes → use the interface
│ └─ No → use generics
When NOT to Use Generics
func Sum[T constraints.Integer | constraints.Float](xs []T) T {
var t T
for _, x := range xs { t += x }
return t
}
func SumInts(xs []int) int {
var t int
for _, x := range xs { t += x }
return t
}
"Write code, don't design types." — Griesemer & Taylor
When Generics Pay Off
- A library function the standard library would have written generically:
slices.Index, maps.Keys, slices.SortFunc.
- Concurrent-safe data structures (typed sets, ordered maps) where boxing into
any would be both ugly and slow.
- Map/Reduce-style helpers that genuinely apply to many element types.
Type Parameter Naming
| Name | Typical use |
|---|
T | General element / first type |
K | Map key |
V | Map value |
E | Element of a collection |
R | Result of a transform |
Multi-letter names are reserved for constraints where the meaning is non-obvious:
func Marshal[Opts encoding.MarshalOptions](v any, opts Opts) ([]byte, error)
Constraint Composition
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~float32 | ~float64
}
func Sum[T Numeric](xs []T) T {
var t T
for _, x := range xs { t += x }
return t
}
~int means "anything whose underlying type is int" — covers type Celsius int.
| unions widen the set.
- Prefer
cmp.Ordered (Go 1.21+) over rolling your own.
Read references/constraints.md for the constraint catalogue, when ~ matters, and how type inference interacts with constraints.
Common Pitfalls
Don't Wrap Stdlib Types Generically
type Set[T comparable] struct {
m map[T]struct{}
}
seen := map[string]struct{}{}
seen["a"] = struct{}{}
A generic wrapper around map[T]struct{} is only worth it if you keep it for many call sites and provide methods that pay for the indirection (e.g., Union, Intersect).
Don't Use Generics for Interface Satisfaction
func Process[T io.Reader](r T) error { ... }
func Process(r io.Reader) error { ... }
Don't Over-Constrain
func Contains[T interface{ ~int | ~string }](xs []T, t T) bool { ... }
func Contains[T comparable](xs []T, t T) bool { ... }
Read references/generics-vs-interfaces.md when interfaces and generics both seem to fit, and you have to choose.
Type Aliases vs Definitions
type Old = pkg.New
type Old pkg.New
Type aliases (=) are for package migrations and gradual API moves. For new types, use a definition.
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|
| Generic for a single instantiation | Indirection without payoff | Concrete code |
| Generic where an interface fits | Type parameter is just io.Reader in disguise | Accept the interface |
interface{ ~int } when comparable suffices | Restricts callers, no benefit | Loosen the constraint |
Custom Numeric constraint | cmp.Ordered exists | Standard constraint |
Set[T] wrapper around map[T]struct{} | Two-line struct, no methods | Use the map directly |
| Generic function with two type params, neither used | The compiler can infer nothing | Drop one or both |
Verification Checklist
References