Generics
Why generics?
Section titled “Why generics?”Before Go 1.18, writing a function that worked on multiple types required either code duplication or interface{} plus type assertions at runtime, losing type safety. Generics solve this: write once, the compiler generates type-specific code and catches misuse at compile time.
For example, a Max function that works for both int and float64 previously required either two identical functions or an interface{} parameter that silently accepted the wrong type. With generics, one function covers both — and the compiler rejects any call with an unsupported type.
Type parameters
Section titled “Type parameters”The syntax for a generic function is func FuncName[T Constraint](args) returnType. The [T Constraint] section is called the type parameter list. T is a placeholder that the compiler substitutes with a concrete type at each call site.
func Max[T Number](a, b T) T { if a > b { return a } return b}At the call site, Go usually infers T from the argument types. You can write Max(3, 7) and the compiler infers T is int — no need to write Max[int](3, 7) explicitly. Explicit type arguments are only needed when inference is ambiguous or when calling with no arguments.
Constraints
Section titled “Constraints”A constraint is an interface that restricts which types may be substituted for a type parameter. Constraints appear after the type parameter name in the [T Constraint] list.
any— the most permissive constraint; equivalent tointerface{}. Allows any type, but limits what operations you can perform on values of typeT(only operations valid for all types: assignment, passing to functions, etc.).comparable— any type that supports==and!=. Use it when your generic function needs to compare values for equality.
You can write custom constraints using interface union syntax:
type Number interface { ~int | ~float64}The ~ prefix means “any type whose underlying type is int or float64.” This is the tilde operator: without it, only the exact named type matches; with it, custom types like type Celsius float64 also satisfy ~float64. This makes constraints work naturally with defined (named) types such as type Celsius float64 — a true alias, type C = float64, is already the same type as float64 and never needs ~.
Constraint interfaces are not ordinary types
Section titled “Constraint interfaces are not ordinary types”A constraint interface that lists type terms with | (or ~) can only be used inside a [T Constraint] list — never as the type of an ordinary variable, field, or parameter, even though it is written with interface { ... } syntax:
type Number interface { ~int | ~float64}
var x Number // BUG: constraint interfaces aren't usable this wayVerified on 1.26.5, this is a compile error:
cannot use type Number outside a type constraint: interface contains type constraintsAn interface without type terms (just methods, like error or io.Reader) can always be used as an ordinary type — this restriction only applies to interfaces containing |-separated type terms, because those exist purely to describe compile-time constraints, not runtime interface values.
comparable’s runtime nuance
Section titled “comparable’s runtime nuance”comparable looks like it guarantees == never fails, but it does not — not fully. Since Go 1.20, any (and any interface type) satisfies comparable, because an interface value’s static type always supports == syntactically. Whether the comparison actually succeeds depends on the dynamic value stored inside it at runtime:
func Equal[T comparable](a, b T) bool { return a == b}
var a, b any = []int{1, 2}, []int{1, 2}Equal(a, b) // compiles fine -- any satisfies comparableVerified on 1.26.5, calling this panics, because a slice is not comparable even though the interface holding it is:
panic: runtime error: comparing uncomparable type []intOrdinary comparable types (int, string, structs of comparable fields) never hit this — the risk only appears when T is instantiated with any or another interface type and the caller doesn’t control what ends up inside it.
Built-in constraints from cmp
Section titled “Built-in constraints from cmp”Go 1.21 introduced the cmp package, which exports cmp.Ordered — the idiomatic constraint for any type that supports <, <=, >, >=, ==. It covers all integer, float, and string types. Using cmp.Ordered is cleaner than writing out the full union ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ... | ~float64 | ~string manually.
import "cmp"
func Min[T cmp.Ordered](a, b T) T { if a < b { return a } return b}The playground snippet below uses a local Number interface instead of cmp.Ordered so the constraint is visible in the same file.
Before cmp.Ordered existed in the standard library (Go 1.21), the community constraint package golang.org/x/exp/constraints filled this role — its constraints.Ordered is the same shape and predates the stdlib version. New code should use cmp.Ordered; constraints.Ordered still shows up in code written for Go 1.18–1.20 or in packages that haven’t migrated, and the two are interchangeable where you control both ends.
Generic types
Section titled “Generic types”You can parameterize structs as well as functions. Declare the type parameter after the type name:
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) { if len(s.items) == 0 { var zero T return zero, false } last := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return last, true}Methods on a generic type repeat the type parameter in the receiver — (s *Stack[T]) — but do not add a new constraint. The constraint is fixed once when the type is declared.
Pop above uses var zero T to produce the zero value of whatever T turns out to be — 0 for numbers, "" for strings, nil for slices and pointers. *new(T) does the same thing: new(T) allocates a zeroed T and returns a *T, and dereferencing it gives the value. Both are equivalent and appear in real code; var zero T is the more common idiom because it reads directly as “the zero value of T” without the extra allocate-then-dereference step.
Instantiation is explicit; inference is not universal
Section titled “Instantiation is explicit; inference is not universal”A generic type never infers its type argument — every use must instantiate it explicitly, including a bare variable declaration:
type Stack[T any] struct{ items []T }
var s Stack // BUG: missing a type argumentVerified on 1.26.5:
cannot use generic type Stack[T any] without instantiationThe fix is var s Stack[int] (or whatever element type you need). This is different from a generic function, which usually doesn’t need an explicit type argument because Go infers T from the arguments at the call site — but that inference only looks at arguments, never at the expected return type:
func Zero[T any]() T { var zero T return zero}
var x int = Zero() // BUG: nothing to infer T fromVerified on 1.26.5:
in call to Zero, cannot infer T (declared at ./main.go:5:11)Even though the result is assigned to an int, Go does not perform return-type inference — Zero[int]() is required.
Generic type aliases (Go 1.24)
Section titled “Generic type aliases (Go 1.24)”Since Go 1.24 a type alias may take type parameters, which is handy for shortening a long instantiation without creating a new named type:
type Set[T comparable] = map[T]struct{}
var seen Set[string] = map[string]struct{}{} // same type, just a shorter nameSelf-referential constraints (Go 1.26)
Section titled “Self-referential constraints (Go 1.26)”A generic type may refer to itself in its own type parameter list. This expresses “a type that can be combined with values of its own type” — the classic Adder shape:
package main
import "fmt"
type Adder[A Adder[A]] interface { Add(A) A}
func Sum[A Adder[A]](xs ...A) A { var acc A for _, x := range xs { acc = acc.Add(x) } return acc}
type Money int
func (m Money) Add(o Money) Money { return m + o }
func main() { fmt.Println(Sum(Money(5), Money(10), Money(20)))} Generic methods (Go 1.27)
Section titled “Generic methods (Go 1.27)”Until Go 1.26, only functions and types could declare type parameters; a method could only use the parameters of its receiver. Go 1.27 lifts that restriction: a method may declare its own type parameters. The standard library uses it in math/rand/v2, where (*Rand).N now mirrors the top-level rand.N:
// math/rand/v2, Go 1.27func (r *Rand) N[Int intType](n Int) Int
r := rand.New(rand.NewPCG(1, 2))d := r.N(10 * time.Second) // Int inferred as time.Durationi := r.N[int64](100) // or explicitYour own types can do the same:
package main
import ( "fmt" "math/rand/v2" "time")
type Registry struct{ items map[string]any }
// Get declares its own type parameter T — allowed since Go 1.27.func (r *Registry) Get[T any](key string) (T, bool) { v, ok := r.items[key].(T) return v, ok}
func main() { reg := &Registry{items: map[string]any{"port": 8080, "host": "localhost"}}
port, ok := reg.Get[int]("port") fmt.Println(port, ok)
host, ok := reg.Get[string]("host") fmt.Println(host, ok)
_, ok = reg.Get[int]("host") // wrong type: zero value, false fmt.Println(ok)
r := rand.New(rand.NewPCG(1, 2)) d := r.N(10 * time.Second) // Int inferred as time.Duration i := r.N[int64](100) // or explicit fmt.Println(d < 10*time.Second, i < 100)} Two rules to remember: interface methods cannot declare type parameters, and a generic method therefore cannot be used to satisfy an interface. Generic methods need Go 1.27+ (go 1.27 in go.mod).
Function type inference, generalized (Go 1.27)
Section titled “Function type inference, generalized (Go 1.27)”Inference for a generic function was originally limited to call sites. Two other contexts now infer the type arguments too, added in two separate releases:
- Assignment to a variable of a matching function type — this has worked since Go 1.21.
- An explicit conversion to a matching function type — this is the Go 1.27 addition.
func Max[T cmp.Ordered](a, b T) T { /* ... */ }
var maxInt func(int, int) int = Max // T inferred as int (Go 1.21+)sortBy := (func(string, string) string)(Max) // T inferred as string (Go 1.27)The assignment form compiles today on this course’s Go 1.26.5 toolchain (with go 1.21 or later in go.mod). The conversion form does not: on 1.26.5 the compiler reports cannot use generic function Max without instantiation. It is confirmed working on Go 1.27 via the go.dev playground — the code above only runs there.
package main
import "fmt"
type Number interface { ~int | ~float64}
func Max[T Number](a, b T) T { if a > b { return a } return b}
func Map[T, U any](slice []T, f func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = f(v) } return result}
func Filter[T any](slice []T, pred func(T) bool) []T { var result []T for _, v := range slice { if pred(v) { result = append(result, v) } } return result}
func main() { fmt.Println(Max(3, 7)) fmt.Println(Max[float64](3.14, 2.71))
nums := []int{1, 2, 3, 4, 5} doubled := Map(nums, func(n int) int { return n * 2 }) fmt.Println(doubled)
words := []string{"go", "generics", "fun", "code"} long := Filter(words, func(s string) bool { return len(s) > 3 }) fmt.Println(long)} When not to use generics
Section titled “When not to use generics”Generics solve a specific problem: the same algorithm, written once, applied to several concrete types with compile-time checking. They are the wrong tool when:
- A single concrete type is all you ever need. A
Stackused only forintin one package doesn’t need a type parameter — add it later if a second use case actually shows up. - Runtime polymorphism is what you actually want. If callers need to plug in different behaviors at runtime (a
Reader, aHandler, a plugin), that’s an interface, not a type parameter — generics are resolved at compile time, so they can’t express “decide which implementation at runtime” the way an interface value can. - The function body barely uses
Tat all. A function that only ever passesTthrough unchanged (noT-specific operation) sometimes reads more clearly withanyand a type assertion at the one place it matters, rather than threading a type parameter through the whole call chain for one narrow use. - The constraint would need to be enormous or contradictory just to make one function generic — that’s usually a sign two genuinely different algorithms are being forced into one shape.
flowchart LR
subgraph Distinct["different GC shapes -- separate compiled code"]
IntCall["Max[int](3, 7)"] --> IntCode["compiled Max for int"]
F64Call["Max[float64](3, 7)"] --> F64Code["compiled Max for float64"]
end
subgraph Shared["pointer-shaped types -- one shared stencil"]
PtrACall["Max[*A](...)"] --> SharedCode["compiled Max for pointer shape"]
PtrBCall["Max[*B](...)"] --> SharedCode
SharedCode --> Dict["runtime dictionary picks *A vs *B behavior"]
end Reading AI-generated code
Section titled “Reading AI-generated code”When a generated snippet uses type parameters, check for these before trusting it:
- Is a constraint interface (one with
|or~terms) ever used as the type of a variable, field, or parameter, instead of only inside[T Constraint]? That’s a compile error, not a runtime surprise. - Is a generic type used without a type argument (
var s Stackinstead ofvar s Stack[int])? Types never infer — only function calls do, and only from arguments. - Does code call
errors.Ason the result ofEqual[T comparable]or similar and assume==can never panic? IfTcan beanyor another interface type, a non-comparable value inside it panics at runtime. - Does a generic function exist where a single concrete type, or a plain interface, would say the same thing with less machinery?
- Is
var zero T(or*new(T)) used to build a “no result” case, but the caller then treats the zero value as if it meant something different from “absent” — e.g.0meaning both “no value” and “value is zero”?
🐛 Spot the bug
func Equal[T comparable](a, b T) bool { return a == b}
func main() { var a, b any = []int{1, 2}, []int{1, 2} fmt.Println(Equal(a, b))}Show the answer
any satisfies comparable since Go 1.20 — the constraint check succeeds at compile time. But == between two any values compares whatever concrete value is stored inside them at runtime, and a []int is not comparable. Verified on 1.26.5, this panics: panic: runtime error: comparing uncomparable type []int. comparable guarantees the code compiles, not that every instantiation is safe to run — this only bites when T ends up being any or another interface type holding a slice, map, or func.
package main
import "fmt"
// Zero returns the zero value of T -- var zero T is the idiomatic form.func Zero[T any]() T { var zero T return zero}
// ZeroAlt is equivalent using *new(T): new(T) allocates a T and zeroes it,// then *new(T) dereferences to get the value. var zero T is more common.func ZeroAlt[T any]() T { return *new(T)}
// Equal works for any comparable type -- including plain values, where == never panics.func Equal[T comparable](a, b T) bool { return a == b}
func main() { fmt.Println(Zero[int](), Zero[string](), Zero[[]int]() == nil) fmt.Println(ZeroAlt[int](), ZeroAlt[string]() == "")
fmt.Println(Equal(3, 3)) fmt.Println(Equal("go", "rust"))
// any satisfies comparable since Go 1.20 -- fine as long as the // dynamic value inside it is itself comparable. var a, b any = 3, 3 fmt.Println(Equal(a, b))} Trade-offs
Section titled “Trade-offs”| What you get | Benefit | Cost |
|---|---|---|
| type parameters | code reuse without interface{} | more compile time, more complex syntax |
| type constraints | compile-time type safety | you must define the constraint interface before using it |
| comparable constraint | lets you use == on a generic type | no support for deep equality (slice, map) |
| ~T (tilde/approximation) | constraint covers named types too, e.g. type MyID int | harder to read for beginners |
Common Misconceptions
Section titled “Common Misconceptions”- generics bloat the binary a lot — Go uses GC-shape stenciling: types that share the same pointer/interface shape share a single instantiation and pass type info through a dictionary at runtime; it does not expand like C++ templates.
- a generic function is always faster than an interface — because pointer/interface shapes share an instantiation through a dictionary, generics can even be slower than an interface in that case; value types (
int, structs) get their own dedicated code per type, so they stay fast and genuinely avoid boxing. - generics are like C++ templates — Go generics check constraints at definition time, not at instantiation — the error messages are far better.
- you should always prefer generics over interfaces — interfaces are the better fit for runtime polymorphism; generics are the better fit for compile-time type-safe algorithms.
💡 Real-world example
slices: the generic
slices.Sort[S ~[]E, E cmp.Ordered](s S)— a type-safe sort that works on any ordered type without casting.