Generics arrived in Go 1.18 (2022); by 2026, the patterns settled. The community’s stance is consistent: use them where they help, don’t where they don’t. This post is the working set.
When generics help
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, len(xs))
for i, x := range xs {
out[i] = f(x)
}
return out
}
doubled := Map([]int{1, 2, 3}, func(x int) int { return x * 2 })
Without generics: per-type Map function or []interface{} casts. Generics: clean, type-safe, no casts.
Standard library uptake
The standard library added slices, maps, cmp, iter packages:
import "slices"
import "maps"
slices.Contains(xs, 5)
slices.Sort(names)
slices.Index(xs, target)
m := maps.Clone(other)
keys := maps.Keys(m)
Use these. They cover ~80% of what teams used to roll themselves.
Constraints
type Numeric interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Sum[T Numeric](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
~int matches int AND any type whose underlying type is int.
golang.org/x/exp/constraints (still useful) defines Ordered, Integer, Float, etc.
Repository pattern
type Repo[T any] struct { db *sql.DB; table string }
func (r *Repo[T]) FindByID(ctx context.Context, id int64) (*T, error) {
var t T
// ... reflection or sqlx-style scan ...
return &t, nil
}
Generic repo for many entity types. Watch out: pure-generic CRUD often gets messy. Many shops keep concrete repos for clarity.
Functional helpers
Common ones:
func Filter[T any](xs []T, p func(T) bool) []T { /* ... */ }
func Reduce[T, A any](xs []T, init A, f func(A, T) A) A { /* ... */ }
func GroupBy[T any, K comparable](xs []T, key func(T) K) map[K][]T { /* ... */ }
Use sparingly. for x := range xs with an explicit append is often clearer than Map/Filter/Reduce chains.
Sets
type Set[T comparable] struct { m map[T]struct{} }
func NewSet[T comparable](xs ...T) *Set[T] {
s := &Set[T]{m: make(map[T]struct{}, len(xs))}
for _, x := range xs { s.m[x] = struct{}{} }
return s
}
func (s *Set[T]) Add(x T) { s.m[x] = struct{}{} }
func (s *Set[T]) Contains(x T) bool { _, ok := s.m[x]; return ok }
func (s *Set[T]) Remove(x T) { delete(s.m, x) }
Useful. Generics make this clean.
Channels and goroutines
type Pipeline[T, U any] struct {
work chan T
out chan U
}
func (p *Pipeline[T, U]) Run(workers int, f func(T) U) {
for i := 0; i < workers; i++ {
go func() {
for t := range p.work {
p.out <- f(t)
}
}()
}
}
Worker pools that don’t lose type info.
Type sets vs interfaces
type Stringer interface { String() string } // method set
type Numeric interface { ~int | ~float64 | ... } // type set
Method sets define behavior; type sets define which concrete types are allowed. Both are valid constraints.
When generics hurt
1. One concrete type
// BAD
type Repo[T any] struct {...}
var userRepo Repo[User]
// ... only ever used with User ...
// GOOD
type UserRepo struct {...}
If you have one type, write one type.
2. Generic returns when interface{} would do
func MustGet[T any](key string) T for config: rarely justifies the syntax overhead vs string specific.
3. Generic method receivers (don’t exist)
Methods can’t introduce new type parameters beyond the receiver’s. Workaround: top-level functions.
4. Reflection + generics
Generics don’t replace reflection (no field access, no struct inspection). For struct-heavy code: reflection still wins.
5. Constraint complexity
func F[T interface{ ~[]E; comparable }, E any](xs T, target E) bool { ... }
If your constraint reads like a riddle, consider concrete types.
Performance
Go generics use “GC stenciling” — different methods generated per pointer-shape. Roughly:
intvsint64vsstringuse different codegen.*Tfor any T shares.
Performance is usually within 5–10% of hand-written specialization. Negligible. If profiling shows it matters: hand-roll.
iter package (Go 1.23+)
import "iter"
func Numbers() iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < 10; i++ {
if !yield(i) { return }
}
}
}
for n := range Numbers() {
fmt.Println(n)
}
Generic iterators. Range over functions. Composable. The slices/maps packages now expose iterator versions too.
Common mistakes
1. Genericizing too early
YAGNI. Wait for the second use case before generic-ifying.
2. Constraint over-engineering
15-line constraint where comparable would do. Keep it minimal.
3. Generic interfaces holding generic state
Get into “where do I store this T?” trouble. Often: a non-generic interface + concrete generic struct is cleaner.
4. Pointer vs value confusion
type Cache[T any] struct { ... }
func (c *Cache[T]) Get(k string) T { ... }
If T is large, you’re copying it. If pointer, shared mutability. Decide deliberately.
5. Forgetting comparable
Map keys / set elements need comparable. Compiler tells you, but the constraint isn’t transitive — explicitly require it.
What I’d ship today
For Go services:
- slices, maps, cmp — use them.
- Domain-specific generics when there’s a clear duplication.
- Repository pattern: judgment call; concrete is fine for most.
- Resist generic-by-default; concrete types win readability.
Read this next
If you want my Go generic helpers (Set, Map/Filter/Reduce that work, repos), it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .