Go Generics Deep Dive
How Go's generics work in practice: type parameters, constraints, the constraints package, and where generics shine versus interfaces or code generation.
What you'll learn
- ✓How type parameters are declared in Go
- ✓Writing and reading constraint interfaces
- ✓The role of comparable and ordered constraints
- ✓When generics are clearer than interfaces
- ✓Performance characteristics of generic code
Prerequisites
- •Comfort with Go interfaces and basic syntax
What and Why
Generics arrived in Go 1.18 after years of debate. They let you write functions and types that work uniformly across many element types without losing static type safety. Before generics, you would either duplicate code, use interface{} with type assertions, or generate code at build time. None of those were great.
Generics are intentionally less expressive than templates in C++ or generics in Java. They prioritize predictable behavior, fast compile times, and code that still reads like Go.
Mental Model
A type parameter is declared in square brackets after the function or type name, with a constraint that says what operations are valid.
func Map[T any, U any](xs []T, f func(T) U) []U
^^^^^^^^^^^^^^
type parameters with constraints
(any = empty interface)
func Sum[T int | float64](xs []T) T
^^^^^^^^^^^^^^^^^^^^^^^
constraint is a union type:
T must be int OR float64 A constraint is just an interface, possibly with a type set. The any keyword is an alias for interface{}. The comparable constraint is built-in and means == and != work on the type.
Hands-on Example
A small generic toolkit.
package main
import (
"fmt"
"golang.org/x/exp/constraints"
)
func Map[T any, 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
}
func Filter[T any](xs []T, pred func(T) bool) []T {
out := xs[:0:0]
for _, x := range xs {
if pred(x) {
out = append(out, x)
}
}
return out
}
func Sum[T constraints.Ordered](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
// A small generic Set type
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
func main() {
nums := []int{1, 2, 3, 4, 5}
squared := Map(nums, func(n int) int { return n * n })
evens := Filter(nums, func(n int) bool { return n%2 == 0 })
fmt.Println(squared, evens, Sum(nums))
s := Set[string]{}
s.Add("ada"); s.Add("bob")
fmt.Println(s.Has("ada"))
}
Three observations. Type inference handles most calls; you almost never need to spell out the type arguments. The Set type is a generic map alias with methods. constraints.Ordered is a constraint from the golang.org/x/exp/constraints package that includes integers, floats, and strings (anything < works on).
Common Pitfalls
- Over-genericizing: not every helper should be generic.
func sum(xs []int) intis shorter and clearer thanfunc Sum[T constraints.Integer](xs []T) Twhen you only ever pass[]int. - Methods cannot add type parameters: you can have generic types with methods, but the methods cannot introduce new type parameters. Move that logic to a top-level function.
anydefeats inference: declaring slices as[]anyforces callers to box every element, losing the benefit of generics. Use specific types or type parameters.- Comparison vs ordering:
comparableallows==, not<. Use theconstraints.Orderedconstraint when you need ordering. - Reflection still exists: do not assume generics replace reflection. Some patterns (deep equality, encoding) still need it.
Practical Tips
Use generics when the algorithm is the same across types. Container types (sets, queues, ring buffers), slice utilities, and channel helpers are great fits.
Use interfaces when you care about behavior (io.Reader, http.Handler) rather than the shape of values. Interfaces remain the right tool for plug-in points.
When writing a constraint, start with the smallest type set that lets the body compile. Wider constraints make APIs more flexible; narrower ones surface compiler errors sooner.
Performance-wise, the Go compiler currently uses a hybrid of monomorphization and dictionary passing. The cost depends on the constraint, but for typical generic helpers it is close to hand-written code. Benchmark before assuming overhead.
For library authors, expose generic functions sparingly in public APIs. Each one becomes part of your stability contract. Internal helpers are a great place to experiment.
Wrap-up
Go’s generics are deliberately modest, and that is a feature. They give you parametric polymorphism for the obvious cases (containers, slice helpers, numeric routines) without inventing a new language. Reach for them when the same algorithm applies to many types, prefer interfaces when behavior is the abstraction, and let type inference keep call sites clean. Used with restraint, generics quietly remove a layer of boilerplate without changing how Go feels.
Related articles
- Go Go Generics: Practical Patterns and Constraints
Learn practical Go generics patterns including type constraints, generic data structures, utility functions, and when to use generics vs interfaces.
- Go Real-World Go Generics Examples Beyond Tutorials
Practical Go generics patterns for production code: type-safe collections, result types, repository layers, and functional helpers.
- Go Go Concurrency: Fan-Out, Pipeline, and Worker Pool
Master advanced Go concurrency patterns including fan-out/fan-in, pipelines, and worker pools with practical examples and production-ready code.
- Go Go Database Patterns with sqlx and pgx
Master Go database patterns using sqlx and pgx for PostgreSQL including connection pooling, transactions, batch operations, and repository design.