Skip to content

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.

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.

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 to interface{}. Allows any type, but limits what operations you can perform on values of type T (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 way

Verified on 1.26.5, this is a compile error:

cannot use type Number outside a type constraint: interface contains type constraints

An 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 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 comparable

Verified 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 []int

Ordinary 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.

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.

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 argument

Verified on 1.26.5:

cannot use generic type Stack[T any] without instantiation

The 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 from

Verified 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.

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 name

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)))
}

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.27
func (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.Duration
i := r.N[int64](100) // or explicit

Your 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)
}

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 Stack used only for int in 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, a Handler, 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 T at all. A function that only ever passes T through unchanged (no T-specific operation) sometimes reads more clearly with any and 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
GC-shape stenciling: each distinct shape gets its own compiled code; same-shaped types share one, plus a dictionary

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 Stack instead of var s Stack[int])? Types never infer — only function calls do, and only from arguments.
  • Does code call errors.As on the result of Equal[T comparable] or similar and assume == can never panic? If T can be any or 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. 0 meaning 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))
}
What you getBenefitCost
type parameterscode reuse without interface{}more compile time, more complex syntax
type constraintscompile-time type safetyyou must define the constraint interface before using it
comparable constraintlets you use == on a generic typeno support for deep equality (slice, map)
~T (tilde/approximation)constraint covers named types too, e.g. type MyID intharder to read for beginners
  • 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.

In func Max[T Number](a, b T) T, what does [T Number] mean?
Does this compile?
type Number interface {
    ~int | ~float64
}

func main() {
    var x Number
}
What happens?
type Stack[T any] struct{ items []T }

func main() {
    var s Stack
}
What constraint allows any type that supports == and !=?
At the call site Max(3, 7), do you need to write Max[int](3, 7)?
A generic Stack[T] has a Pop method returning (T, bool). What does var zero T inside Pop produce when T is []int?