Generics and modern Go idioms
Type parameters and constraints, when generics clarify rather than over-abstract, iterator functions with range-over-func, and small-interface design.
Type parameters and constraints
// Map works for any pair of types; the constraint is implicit "any"
func Map[T, U any](in []T, f func(T) U) []U {
out := make([]U, 0, len(in))
for _, v := range in {
out = append(out, f(v))
}
return out
}
// a union constraint: only the listed types satisfy it
type Number interface {
~int | ~int64 | ~float64 // ~ includes named types with that underlying type
}
func Sum[T Number](nums []T) T {
var total T // the zero value of the parameter type
for _, n := range nums {
total += n // legal because every member supports +
}
return total
}
// constraints can require methods as well
type Key interface {
comparable // required for map[K] and ==
String() string
}
func IndexBy[K Key, V any](items []V, key func(V) K) map[K]V {
m := make(map[K]V, len(items))
for _, it := range items {
m[key(it)] = it
}
return m
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}), Sum([]float64{1.5, 2.5}))
fmt.Println(Map([]int{1, 2}, func(n int) string { return strconv.Itoa(n) }))
}- A constraint is an interface used in a type parameter list.
anyis the empty constraint,comparableadds==and map-key eligibility. - Go has no type inference for type arguments from the return type, so
Map([]int{1}, f)is fine but a call whose only unbound parameter appears in the result needs explicit braces. - Instantiation happens at compile time with no boxing for value types:
Sum([]int{...})compiles to integer addition directly. - Method sets still decide interface satisfaction, and a generic function cannot have its own type parameters on a method — methods may only use the type parameters of the receiver type.
Iterator functions
// a standard library iterator: receives a yield function, returns stop
func Backwards[T any](s []T) iter.Seq[T] {
return func(yield func(T) bool) {
for i := len(s) - 1; i >= 0; i-- {
if !yield(s[i]) { // yield returns false when the caller stopped
return
}
}
}
}
func main() {
// range over the function: break, continue and return all work
for v := range Backwards([]string{"a", "b", "c"}) {
fmt.Println(v) // c, b, a
}
// the standard helpers compose
seq := slices.Values([]int{3, 1, 2})
for n := range slices.Sorted(seq) {
fmt.Println(n) // 1, 2, 3
}
}| Signature | Meaning |
|---|---|
iter.Seq[V] | func(yield func(V) bool) — a sequence of values |
iter.Seq2[K, V] | Two values per step, as in a key and a value |
yield returns false | The consumer broke out; stop producing and return |
slices.Values | Turns a slice into an iterator |
maps.All | Iterates a map in the usual randomised order |
Iterator functions let lazy pipelines be written with ordinary for loops and no interface allocation, and a break in the loop simply stops the producer. The cost is that the iterator body runs on the caller's goroutine, so any blocking work inside it blocks the caller.
When not to use generics
- Generics are for containers, algorithms over a type parameter, and eliminating identical copies — not for making a two-case function look clever.
- If the type parameter appears once in the body, you probably wanted
anyor a concrete type. One use means no reuse. - A generic type whose methods all need
anyconversions has more machinery than the problem. Write the concrete version first and generalise only when a second caller appears. - Interfaces remain the right tool for behaviour: a constraint that lists five methods is a sign the abstraction belongs in an interface parameter instead.
- The standard library already generalised the common cases —
slices.Sort,maps.Keys,sync.Pool,atomic.Pointer[T]— so check before writing your own helper collection.
// before: three near-identical functions
func SumInts(v []int) int
func SumFloats(v []float64) float64
// after: one function and a union constraint
func Sum[T Number](v []T) T
// reconsidering: is a generic really clearer here?
type Pair[A, B any] struct{ A A; B B } // usually a named struct with real field names is better
// what generics are genuinely good at: type-safe containers and caches
type Stack[T any] struct{ items []T }
func (s *Stack[T]) Push(v T) { s.items = append(s.items, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}💡
The idiomatic verdict on generics in Go is the same as on interfaces: write the concrete version twice before you abstract. Two real call sites tell you what the type parameter is for, and the abstract version is usually shorter than the attempt to guess it in advance.
FAQ
What does the tilde in a constraint mean?
~int means "any type whose underlying type is int", so a named type such as type Celsius float64 satisfies ~float64. Without the tilde the constraint matches only the exact predeclared type.Can I add methods to a generic type?
Yes, on the generic type itself, and the methods can use the receiver's type parameters. You cannot declare new type parameters on a method, so a method cannot introduce a type that the type does not already have.
Related
Structs, methods and interfaces Profiling, performance and deployment
Last refreshed 2026-09-18.