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. any is the empty constraint, comparable adds == 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
    }
}
SignatureMeaning
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 falseThe consumer broke out; stop producing and return
slices.ValuesTurns a slice into an iterator
maps.AllIterates 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 any or a concrete type. One use means no reuse.
  • A generic type whose methods all need any conversions 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.

Structs, methods and interfaces Profiling, performance and deployment

Last refreshed 2026-09-18.