Generics let one piece of code work over many types while the compiler still checks them: a Stack[int] accepts only int, needs no type assertions and stores values inline, while a []any accepts anything and checks nothing until run time.
Type safety: with []any a wrong push surfaces as a panicking v.(int) in production; with Stack[int] it is a compile error.
No boxing: a non-pointer value stored in any is usually copied to the heap (small integers and zero values are the exception); a []int stores the values inline.
Signatures that document themselves: func Keys[K comparable, V any](m map[K]V) []K says what goes in and comes out; func Keys(m any) []any does not.
Before Go 1.18 the alternatives were interface{}, code generation or a copy per type. any is still right when the values really are mixed: a decoded JSON document, a bag of log fields.
GoMiddle
Generics & Type Parameters
Type parameters, constraints, type sets, inference, GC shape stenciling
Questions closed
0 / 19
- 0Review
- 2Asked often
Questions
Type parameters go in square brackets before the ordinary parameters (func Map[T, U any](xs []T, f func(T) U) []U) and after a type's name (type Stack[T any] struct{ items []T }). Methods repeat the parameter in the receiver: func (s *Stack[T]) Push(v T).
Every type parameter has a constraint: any for anything, comparable for map keys and ==, or your own interface.
Calls usually infer the type arguments: Map(ids, strconv.Itoa). A generic type must be instantiated where it is used: var s Stack[int]; a bare Stack does not compile.
A method uses the receiver's T; its own type parameters are allowed only from Go 1.27, and such a method cannot satisfy an interface.
any promises nothing about T, so the body may use only operations that every type supports, and > is not one of them: slices and structs have no order. Constrain T with cmp.Ordered and > becomes legal.
**comparable**: adds == and !=; enough for map keys, Index and Contains.
**cmp.Ordered** (Go 1.21): every integer, float and string type, each with ~; gives <, > and also +.
A union such as ~int | ~float64 gives the operators all its members share: + - * / for a Sum.
For two plain values you need no Max of your own: the built-in max(a, b) exists since Go 1.21, and slices.Max covers a slice.
Declare **var zero T** and return it: a type parameter has no literal for its zero value, because nil, 0 and "" each fit only some types.
nil is the zero only for pointers, slices, maps, channels, funcs and interfaces; 0 only for numbers; "" only for strings. A T any can be any of them.
T{} needs every type in T's constraint to share one struct, array, slice or map underlying type; under any they do not.
The zero is a real value, not an absence, so a lookup returns (T, bool): a found 0 and a missing int are the same bits.
*new(T) is the older one-expression form of the same thing; var zero T reads better.
The ~ means "underlying type": **~string** admits every named type built on string, while a bare string admits only string itself.
A constraint is a set of types. interface{ string } holds exactly one type, so type UserID string is not in it.
~string holds string plus every type X string in any package, and operations on T keep the named type.
The tilde goes only before a type that is its own underlying type, and never before an interface.
cmp.Ordered is spelled ~int | ... | ~string for this reason: it is why slices.Sort takes a []UserID.
Inference uses the types of the arguments you pass, never the type the result is assigned to. A type parameter that appears only in the result has to be written out.
In Map[S, T](xs []S, f func(S) T) the slice gives S and the function gives T, so Map(users, nameOf) needs nothing.
var s *Set[int] = NewSet() still fails with cannot infer T: the left-hand side is not an input.
Partial instantiation is allowed: Map[User](users, f) writes the first parameter and infers the rest.
Since Go 1.21 a generic function value is inferred from its target: var f func(int) string = Format works without Format[int].
Constructors are the common case; so is an argument with no type of its own, such as an untyped nil.
An interface that lists types (a union, or any ~ term) is a constraint only and can never be the type of a value. The ready-made Ordered lives in the standard library as cmp.Ordered since Go 1.21; the old golang.org/x/exp/constraints import is no longer needed.
There is nothing to store in a value's type word for "int or float64", so var n Number is rejected with interface contains type constraints. Ordinary method interfaces have that representation.
If you need a value, use any with a type switch. A constraint is a compile-time filter and nothing more.
Embedding intersects: interface{ cmp.Ordered; fmt.Stringer } is ordered and printable. A union of embedded constraints unions their type sets.
comparable may be embedded but never used as a union term.
A type parameter has only the methods its constraint declares, and a union declares types, not methods; the String both members happen to have is invisible. Embed fmt.Stringer next to the union.
A union term is a non-interface type, optionally with ~, or an interface with no methods: ~string | fmt.Stringer is rejected.
What a union does give you are the operators every member supports: + and < for ~int | ~string, len and indexing for ~[]byte | ~string.
Writing the union and fmt.Stringer in one interface intersects the two, and v.String() then compiles.
If all you want is "anything with String()", that is a plain fmt.Stringer parameter and a type parameter buys nothing. Take the union for the operators.
It prints int other stringer. A type switch matches the exact dynamic type, so MyInt is not int even though its underlying type is; time.Duration lands in fmt.Stringer because it has a String method.
switch v.(type) on a type-parameter value does not compile (cannot use type switch on type parameter value v): a type switch needs an operand of interface type and T is a type parameter, not an interface. Converting to any first is the way around it, and it may box v on the heap.
~ does not exist in type switches: there is no case ~int. Covering named types means listing each one or switching on reflect.TypeOf(v).Kind().
The switch is a run-time check on every call, not a compile-time choice per instantiation.
A type switch over T usually means the behaviour differs per type, which is what an interface method is for.
any(v) == nil is true only for a nil interface. A nil *Config converted to any is a non-nil interface holding a typed nil, so OrDefault returns cfg and c.Timeout dereferences a nil pointer.
Why: an interface value is a pair (dynamic type, value), and it equals nil only when both are empty. After any(v) the type slot holds *Config, so the check can be true only when T is itself an interface type such as error.
Why it slips through: a test with OrDefault[error](nil, def) passes; the bug appears only with a pointer type argument.
The fix: constrain T with comparable and compare with var zero T: for a pointer the zero is nil, for an interface it is a nil interface. The standard cmp.Or(cfg, def) (Go 1.22) does exactly this.
Trade-off: the zero check also treats 0 and "" as unset, and comparable rejects slices and maps. If you mean "nil pointer only", take pointers explicitly: func OrDefault[T any](v, def *T) *T.
Catching it: test generic helpers with a pointer, an interface and a plain value as the type argument; treat any(v) == nil in a generic body as a review smell.
comparable only promises that == compiles. Since Go 1.20 an ordinary interface such as any satisfies it, and for interface values the real check happens at run time.
Comparing two interface values compares their dynamic types first. If that type is a slice, a map or a func, the runtime panics with comparing uncomparable type []int.
A map[any]int with a slice key panics too, with hash of unhashable type []int. Generics did not add the hazard; they stopped hiding it.
To make it impossible again, name the types you support: ~string | ~int64. Or take the comparison from the caller, the way slices.IndexFunc does.
A struct with an any field carries the same trap: it is comparable, and it can still panic.
On a miss Load returns a nil any, and the single-value assertion v.(V) panics with interface conversion: interface {} is nil, not main.User before the caller ever sees ok == false.
Why: a single-value type assertion panics whenever the interface does not hold a V, and a nil interface holds nothing. Generics give Get a typed signature, but sync.Map still stores any inside, so the assertion remains a run-time check.
The fix: the comma-ok form val, _ := v.(V) never panics: on a miss val is the zero V, and ok from Load stays false.
**Interface V**: with Cache[string, error], Put(k, nil) also stores a nil interface. The comma-ok form returns (nil, true) for it, while a fix that only returns early on !ok would still panic there.
Catching it: a test for the miss path, and the forcetypeassert linter in golangci-lint, which flags every unchecked assertion.
It never returns 0, so two rows with the same score each claim to be greater than the other. slices.SortFunc needs a consistent three-way result: negative, zero, positive.
slices.SortFunc is not stable, so equal rows come out in an arbitrary order unless the function breaks the tie.
Return cmp.Compare(a.Score, b.Score) (Go 1.21): it gives -1, 0 or +1, and for floats it orders NaN before every other value instead of breaking the sort.
Break ties explicitly: cmp.Or(cmp.Compare(...), strings.Compare(a.Name, b.Name)); cmp.Or returns its first non-zero argument.
Or keep the input order for ties with slices.SortStableFunc.
The usual origin is a port from sort.Slice, whose callback is a less returning bool.
An iter.Seq[V] is a func(yield func(V) bool): you call yield for each value and stop as soon as it returns false, which means the loop body did break or return. Cleanup goes in a defer inside the iterator, since only the iterator knows what it opened.
Every yield site reads if !yield(v) { return }. Ignoring false panics at run time: range function continued iteration after function for loop body returned false.
One defer f.Close() at the top covers exhaustion, an early break and an error alike; the caller's for loop cannot close a file it never opened.
An adapter such as Filter must pass the outer yield's false inward, or the source keeps producing after the consumer has left.
iter.Pull turns a sequence into next/stop functions for walking two at once; a caller that does not drain it must call stop, or the iterator's deferred cleanup never runs.
maps.All, maps.Keys and slices.Values return these iterators.
Use a type parameter when the code is the same for every type and only the element type changes: containers, slices-style algorithms, operators like +. When the behaviour differs per type, use an interface; when there are only two or three types, concrete code is often clearer.
A parameter that is only threaded through: a Repo[T, ID] whose differences all live in six function fields shares nothing; those closures are the real abstraction, and three concrete repositories read and profile better.
**A type switch on T**: that is any with extra syntax; the behaviour belongs in an interface method.
A single call site: a concrete function made harder to read and to grep.
An interface parameter already works: func Save(w io.Writer) needs no [W io.Writer], unless you must hand the caller's concrete type back.
What earns one: an algorithm where the element type never appears in the logic (slices.Chunk, a work queue), or an operator no interface can express: Sum needs +.
Parameterise the slice as well (func Scale[S ~[]E, E ~int32](s S, c E) S), and the result type is whatever the caller passed. E is still inferred from S, so the call site does not change.
With E = int32 the old result type is literally []int32. A Point goes in because it is assignable to []int32, but that is not the same type, and Norm is declared on Point.
With S a parameter, make(S, len(s)) builds a Point and a Point comes back out.
Inference matches S against Point from the argument, then reads E out of the constraint ~[]E by lining []E up with Point's underlying []int32.
This is why the standard library writes func Sort[S ~[]E, E cmp.Ordered](x S): S is there for the caller's named types, not for the algorithm.
Methods are how types satisfy interfaces, and a generic method would need a body for every U while interfaces are checked at run time. Go 1.27 allows type parameters on concrete methods but keeps the ban for interface methods: a generic method never satisfies an interface.
A type assertion like x.(interface{ Map(func(int) string) *Set[string] }) could demand an instantiation no code in the build ever produced; its method table would have to be generated at run time.
Receiver parameters are fine: Set[int] is one concrete type with a finite method set, so func (s *Set[T]) Add(v T) compiles.
The workaround is a package-level function: func MapSet[T, U comparable](s *Set[T], f func(T) U) *Set[U]. You lose method chaining and nothing else.
Go 1.27 (August 2026) allows func (s *Set[T]) Map[U comparable](...); a generic method does not count toward an interface method of the same name, and interface methods still cannot declare type parameters. On Go 1.26 and older use the package-level MapSet.
Take two type parameters ([T any, PT interface{ *T; Validator }]) so the value stays a T while the methods come from *T, which you reach inside the body as PT(&v).
Validate with a pointer receiver belongs to *Row, not to Row, and constraint satisfaction uses that ordinary method-set rule: Row does not satisfy Validator (method Validate has pointer receiver).
T = *Row only moves the problem: var v T is a nil pointer, and you now return a pointer whether the caller wanted one or not.
PT interface{ *T; Validator } is an intersection: the set holds only *T, and it must have Validate. PT(&v) is a free conversion that carries the method set.
Callers still write Decode[Row](b), since PT is inferred from T.
Go compiles a generic function once per GC shape (roughly the underlying memory layout, with all pointer types sharing one shape) and passes each instantiation a hidden dictionary of type information. It is neither C++ templates nor interfaces.
What the compiler emits
First[*A] and First[*B] share one body, First[go.shape.*uint8]; Sum[int] and Sum[float64] get a body each.
Dictionaries (main..dict.First[*main.A]) carry what the shared body needs at run time: the real type for make([]T, n), for converting a T to any, for method lookups.
The binary grows with the number of shapes, not of instantiations, and values stay inline with no per-element boxing.
When it is slower
A method call on a pointer-shaped T first finds the method through the dictionary, then calls indirectly. It cannot be inlined or devirtualised, so it can lose to a plain interface parameter.
Instantiating with an interface type as T can be slower still: the lookup goes through the dictionary and then the interface's own method table.
The fast case is value shapes with operators: Sum[int] compiles to the loop you would write by hand.
To check, go tool nm shows the shapes, -gcflags=-m the inlining decisions, and a benchmark settles the rest.