ToolPopToolPop
Go · Lesson 17 of 22

Generics: when to use, when to skip

8 min readUpdated 25 Jun 2026

Generics landed in Go 1.18 after a decade of community debate. They are a real, useful, well-designed feature. They are also the most overused feature in junior Go code, because once you have a hammer that says [T any], every problem starts looking like a type parameter.

This lesson is the calibrated senior view: when generics genuinely help, when they make the code worse, and the syntax you need so you can read library code without flinching.

The syntax in one example

A generic Map function over slices.

go
func Map[T, U any](s []T, f func(T) U) []U {
    out := make([]U, len(s))
    for i, v := range s {
        out[i] = f(v)
    }
    return out
}
 
doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })

[T, U any] declares two type parameters. any is the constraint, meaning "any type at all". The compiler instantiates a concrete copy of Map for each (T, U) pair you use, which is how Go gets type safety without runtime reflection.

Constraints

A constraint is an interface that says "type parameters must satisfy this".

go
type Number interface {
    ~int | ~int64 | ~float32 | ~float64
}
 
func Sum[T Number](xs []T) T {
    var s T
    for _, x := range xs { s += x }
    return s
}

The ~int syntax means "int or any type whose underlying type is int". Without the tilde, a type Rupees int would not satisfy Number. With it, it does. This is almost always what you want.

The standard library ships constraints.Ordered (in golang.org/x/exp/constraints) for "anything you can use < with". Use it before inventing your own.

go
import "golang.org/x/exp/constraints"
 
func Max[T constraints.Ordered](a, b T) T {
    if a > b { return a }
    return b
}

The built-in comparable constraint (anything you can ==) covers map keys.

go
func Keys[K comparable, V any](m map[K]V) []K {
    out := make([]K, 0, len(m))
    for k := range m { out = append(out, k) }
    return out
}

When generics actually pay off

Three categories, basically. If your use case is not in these, default to "no generic".

1. Generic data structures. A type-safe Set[T], Stack[T], LRUCache[K, V], ordered tree, priority queue. Pre-generics, you wrote one per type or used interface{} and lost type safety. Now you write it once.

go
type Set[T comparable] struct {
    m map[T]struct{}
}
 
func (s *Set[T]) Add(v T) { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }

2. Slice and map utilities. Map, Filter, Reduce, GroupBy, Keys, Values. The slices and maps packages in the standard library (Go 1.21+) are exactly this and you should use them rather than rolling your own.

go
import "slices"
nums := []int{3, 1, 4, 1, 5, 9}
slices.Sort(nums)
i, found := slices.BinarySearch(nums, 4)

3. Mathematical or numeric generic code. Min, Max, Sum, Abs. Things that work the same way for every numeric type and would otherwise be one function per type.

When NOT to use generics

This is the harder skill and the one that separates seniors from juniors.

1. When an interface does the job. If you only need behaviour, not the exact type, use an interface.

go
// no generic needed
func ProcessAll(readers []io.Reader) { ... }
 
// over-engineered
func ProcessAll[T io.Reader](readers []T) { ... }

The generic version gives you nothing the interface version did not, costs you readability, and may produce more compiled code (because the compiler instantiates a copy per type).

2. When you only have one or two call sites. Generics shine for reusable infrastructure. For a helper called from three places in your service, just write three small functions, or one with any and a type assertion, or copy-paste. Code that is harder to read but only saves you ten lines is a bad trade.

3. When the abstraction is leaky. If your generic function only really works for int and string but you wrote it [T any], you have a runtime bomb waiting. Constrain tightly or do not generalise.

The Rob Pike rule

A genuine Rob Pike quote, paraphrased: "If you're writing your second generic, fine. If you're writing your first, think hard about whether you need it."

The rule of thumb: generics are a tool you reach for when you have already written the non-generic version twice, once for User and once for Order, and the third type is on its way. Do not write generics speculatively. Refactor into them when the duplication is real and the abstraction is solid.

Senior rule: generics are a tool for library authors. Application code mostly should not write its own. Use the ones from slices, maps, sync (OnceValue, Map), and well-known third-party libraries. Invent generics only when you have proven the need.

Type inference and explicit parameters

Most calls do not need you to spell out the type parameters; the compiler infers them from the arguments.

go
out := Map(users, func(u User) string { return u.Name }) // T=User, U=string inferred

Sometimes inference fails (especially when the type only appears in the return) and you have to be explicit.

go
empty := slices.Collect[int](nil) // explicit T=int

Knowing how to write the explicit form is useful for diagnosing "cannot infer T" errors, which are the most common generics compilation error in practice.

Performance, briefly

Go's generics use a mix of GC-shape stenciling and dictionary passing. The result: roughly the same speed as the equivalent hand-written code for value types, sometimes a small overhead for pointer/interface types due to the dictionary lookup. Do not pick generics over interfaces for performance; the difference is almost never material.

Real performance gains come from avoiding interface{} boxing in tight loops. A generic Sum[T Number](xs []T) over []int64 does not box, where a Sum(xs []any) would. That can be a 5x improvement for numeric hot paths.

Generics terms

type parameter
A placeholder type declared in square brackets: func Map[T any](...). Instantiated at each call site.
constraint
An interface that limits which types can be used as a type parameter. any, comparable, or custom.
comparable
Built-in constraint for types usable with == and !=. Required for map keys.
constraints.Ordered
Constraint in golang.org/x/exp/constraints for types usable with <, >. Covers numeric and string types.
tilde (~)
Constraint syntax meaning 'this type OR any type whose underlying type is this'. ~int covers type Rupees int.
type inference
Compiler deducing type parameters from argument types so you do not have to spell them out at the call site.

Senior rule: generics are a tool for library authors. Application code mostly should not write its own. Use them, do not invent them.

Common questions

Q.When should I write a generic function?
A.When you find yourself writing the same code for two different types and the only difference is the type. Data structures (set, heap, ring buffer) and slice/map utilities (Map, Filter, Reduce) are the clear wins. For most application code, don't.
Q.What does the constraint `comparable` mean?
A.The type supports == and !=. Required when you use the type as a map key or compare values with ==. Numbers, strings, booleans, channels, pointers, interfaces, structs of comparables. Slices, maps, and functions are NOT comparable.
Chai0/1 done

Watching quietly. Tap me if you want a tip.

Go Playground

Go cannot run natively in a browser. Run copies your code and opens go.dev/play ; paste and click Run there.

Try this (0 of 1 done)

  1. 1

    Predict the output.

    show answer
    // already in the editor