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 Build Tags Explained
Use Go build tags to include or exclude files per OS, architecture, or custom condition. Learn the new //go:build syntax, common patterns, and how tags interact with the test runner.
- Go Go Context Cancellation Patterns
Master Go's context package: propagate deadlines, cancel goroutines safely, and avoid leaks with practical patterns for HTTP, database, and pipeline code.
- Go Go context Package Explained
How to use Go's context package effectively: cancellation, deadlines, propagation, request-scoped values, and the patterns that keep services responsive.
- Go Go database/sql Tutorial
Use Go's standard database/sql package the right way: drivers, connection pools, prepared statements, transactions, context cancellation, and avoiding the classic Rows.Close leak.