Generics

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27. Generics (formally type parameters) let one function or type work for many types while the compiler still checks every use statically. They arrived in Go 1.18 and have grown with almost every release since: better inference (1.21), generic type aliases (1.24), a simpler specification without core types (1.25), self-referential constraints (1.26) and, in Go 1.27, generic methods. The page covers all of it, with the rules from the specification, the real compiler messages for the mistakes you will meet, and guidance on when generics are the right tool and when an interface or plain code is better.

Related pages: Interfaces (type sets build on interfaces), Methods and Receivers, Iterators and Range-over-Func (generic iter.Seq adapters), Collections and Utility Packages (slices, maps, cmp), Functions, Closures and Defer and Types and Values. To compare with other languages' generics see Rust generics, Java generics, C# generics, Kotlin generics, TypeScript generics and C++ templates.

Every example was compiled, vetted with go vet and run with Go 1.27.2; the output shown is the real output (the compiler messages are likewise copied from real runs, using a one-file module whose go.mod is shown below). The example module is:

module example.com/shelf

go 1.27

Why generics, and what they replace

Before Go 1.18 a function that had to work for several types had three options, none good: write one copy per type, accept any (then called interface{}) and assert types at run time, or generate the copies with a tool. The first duplicates code, the second gives up compile-time checking, and the third adds a build step. The Go FAQ explains that Go was first released without generics on purpose, because the designers wanted a design that fitted the rest of the language; see the FAQ entries on type parameters.

package main

import (
	"cmp"
	"fmt"
)

// MaxOf returns the larger of a and b. T may be any ordered type.
func MaxOf[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

// Before Go 1.18 the same idea needed one function per type...
func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func maxString(a, b string) string {
	if a > b {
		return a
	}
	return b
}

// ...or an empty interface, which gives up static type checking.
func maxAny(a, b any) any {
	switch x := a.(type) {
	case int:
		if x > b.(int) {
			return a
		}
		return b
	case string:
		if x > b.(string) {
			return a
		}
		return b
	}
	panic("unsupported type")
}

func main() {
	fmt.Println(maxInt(3, 9), maxString("Dune", "Emma"), maxAny(2, 7))
	fmt.Println(MaxOf(3, 9), MaxOf("Dune", "Emma"), MaxOf(2.5, 1.5))
	// maxAny(2, "x") compiles but panics at run time; MaxOf(2, "x") does not compile.
}
9 Emma 7
9 Emma 2.5

MaxOf is written once and checked at compile time. MaxOf(2, "x") does not compile, whereas maxAny(2, "x") compiles and panics when it runs.

Terminology

Term Meaning

Type parameter list

The bracketed list in a declaration, such as [T, R any] or [S ~[]E, E any]. Every name must be present. Square brackets were chosen because angle brackets make parsing depend on type information (see the FAQ).

Type parameter

A placeholder type declared by the list (T, R). It is a new named type that exists only inside the declaration.

Type constraint

The "type of a type parameter": an interface that states which types may be used as the argument, and which operations the body may use on values of that parameter.

Type argument

The concrete type that replaces a type parameter when the generic function or type is instantiated.

Instantiation

Substituting type arguments for type parameters. A generic function is instantiated explicitly (Map[int, string]) or implicitly through inference. A generic type must always be instantiated: Stack[int].

Type set

The set of all types that satisfy an interface. Constraints are interfaces, so each one denotes a type set.

Generic method

A method that declares type parameters of its own, in addition to those of its receiver type (Go 1.27).

The specification covers all of this in Type parameter declarations, Instantiations and Type inference.

Generic functions

A type parameter list follows the function name. The parameters can be used in the signature and the body.

package main

import (
	"fmt"
	"strconv"
)

// Map applies f to every element of s.
func Map[T, R any](s []T, f func(T) R) []R {
	out := make([]R, 0, len(s))
	for _, v := range s {
		out = append(out, f(v))
	}
	return out
}

// Convert has a type parameter that cannot be inferred from its arguments.
func Convert[To, From ~int | ~int64 | ~float64](v From) To { return To(v) }

type Pages int

func main() {
	pages := []int{412, 1032, 96}

	// Full explicit instantiation: both type arguments are given.
	a := Map[int, string](pages, strconv.Itoa)

	// Partial instantiation: T is given, R is inferred from f.
	b := Map[int](pages, func(n int) float64 { return float64(n) / 100 })

	// Full inference: T from pages, R from strconv.Itoa.
	c := Map(pages, strconv.Itoa)

	fmt.Printf("%q %v %q\n", a, b, c)

	// Only the first type argument is given; From is inferred from the argument.
	x := Convert[float64](Pages(412))
	fmt.Printf("%T %v\n", x, x)

	// Instantiating without calling yields an ordinary function value.
	f := Map[string, int]
	fmt.Printf("%T\n", f)
}
["412" "1032" "96"] [4.12 10.32 0.96] ["412" "1032" "96"]
float64 412
func([]string, func(string) int) []int

Points to notice:

  • T, R any declares two parameters with the same constraint, like a, b int for ordinary parameters.

  • Explicit instantiation lists type arguments left to right. You may stop early (partial instantiation), and the compiler infers the rest. Convert[float64](Pages(412)) supplies To and infers From from the argument, which is why To is declared first: order type parameters so the ones that cannot be inferred come first.

  • An instantiated generic function without a call, such as Map[string, int], is an ordinary function value with a concrete type. A generic function that is not instantiated cannot be used as a value, except where the type can be inferred (see below).

  • Inside the body a value of type T supports exactly the operations that every type in the type set of T supports, nothing more. With any you can assign, pass and return values, take their address and convert them to any.

The zero value, pointers and type switches

There is no nil for an arbitrary T. Use var zero T, a named result, or *new(T). To inspect the dynamic type of a value of type parameter type, convert it to any first, because a type switch directly on a type parameter value is rejected.

package main

import (
	"fmt"
	"reflect"
)

type Pair[K comparable, V any] struct {
	Key K
	Val V
}

// Describe uses any(v) to inspect the dynamic type: a type switch directly on v is not allowed.
func Describe[T any](v T) string {
	switch x := any(v).(type) {
	case int:
		return fmt.Sprintf("int %d", x)
	case string:
		return fmt.Sprintf("string %q", x)
	case fmt.Stringer:
		return "stringer " + x.String()
	}
	return fmt.Sprintf("other %T", v)
}

// Zero returns the zero value of T.
func Zero[T any]() T {
	var z T
	return z
}

// PtrTo returns a pointer to a copy of v.
func PtrTo[T any](v T) *T { return &v }

func main() {
	fmt.Println(Describe(7), Describe("Dune"), Describe(2.5))
	fmt.Printf("%q %v %v\n", Zero[string](), Zero[*int]() == nil, Zero[[]int]() == nil)
	fmt.Println(*PtrTo("Emma"))

	// Type names of instantiated types include their type arguments.
	p := Pair[string, []int]{"Dune", []int{1}}
	fmt.Printf("%T\n", p)
	fmt.Println(reflect.TypeFor[Pair[string, int]]().String(), reflect.TypeFor[Pair[string, int]]().Name())
}
int 7 string "Dune" other float64
"" true true
Emma
main.Pair[string,[]int]
main.Pair[string,int] Pair[string,int]

The %T output shows that the full type arguments are part of the run-time type name, so reflection sees Pair[string,int] and not an erased type (unlike Java; see the FAQ section "How do generics in Go compare to generics in other languages?").

Type inference

Type arguments rarely have to be written. The compiler infers them by unifying the types of the arguments with the types of the parameters, then using the constraints. The specification (Type inference) describes several cooperating mechanisms:

  1. Function argument type inference: typed arguments are matched against parameter types ([]int against []T gives T = int; func(int) string against func(T) R gives R = string).

  2. Constraint type inference: a constraint with a single underlying type, such as ~[]E, connects parameters. Given S = Counts, the compiler finds the underlying type []Pages and deduces E = Pages.

  3. Untyped constants: after typed arguments are processed, untyped constants supply a default type (int, float64, rune, string, …​).

  4. Assignment contexts (since Go 1.21): a generic function that is assigned, returned or passed where a function type is expected is inferred from that type.

Diagram of type inference for the call Map(pages, strconv.Itoa) on func Map[T, R any](s []T, f func(T) R) []R. The argument pages of type []int is unified with []T, giving T equals int. The argument strconv.Itoa of type func(int) string is unified with func(T) R, giving R equals string. The result is the instance Map[int, string]. Two failure cases are shown: Zero() with func Zero[T any]() T cannot infer T because nothing mentions it, and MaxOf(1, two) mixes an untyped int and an untyped string so there is no single T.
package main

import (
	"fmt"
	"maps"
	"slices"
)

type Pages int
type Counts []Pages

// Scale shows constraint type inference: E is not mentioned in any argument
// directly as a type, it is deduced from S's constraint ~[]E.
func Scale[S ~[]E, E ~int | ~float64](s S, k E) S {
	out := make(S, len(s))
	for i, v := range s {
		out[i] = v * k
	}
	return out
}

// Keys returns the keys of any map type M.
func Keys[M ~map[K]V, K comparable, V any](m M) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}

type Stock map[string]int

func main() {
	c := Scale(Counts{100, 200}, 3)
	fmt.Printf("%T %v\n", c, c)

	ks := Keys(Stock{"Dune": 3, "Emma": 1})
	slices.Sort(ks)
	fmt.Println(ks)
	fmt.Println(slices.Sorted(maps.Keys(Stock{"Hamlet": 2})))
}
main.Counts [300 600]
[Dune Emma]
[Hamlet]

Scale is called with a Counts value and an untyped constant. S comes from the first argument, E from the constraint ~[]E, and the constant 3 becomes a Pages. Note that Scale returns S, so the caller keeps its own slice type: main.Counts and not []Pages. This is why the standard slices functions are declared with S ~[]E and not with []E.

Untyped constants and generic function arguments

package main

import (
	"cmp"
	"fmt"
	"slices"
	"strings"
)

func Sum[T int | float64](vs ...T) (total T) {
	for _, v := range vs {
		total += v
	}
	return
}

func Apply[T, R any](v T, f func(T) R) R { return f(v) }

func Double[T int | float64](v T) T { return v * 2 }

func main() {
	// Untyped constants: when several constants of different kinds are used,
	// the default type of the "largest" kind is chosen (Go 1.21).
	fmt.Printf("%T\n", Sum(1, 2, 3))
	fmt.Printf("%T\n", Sum(1, 2.5))
	fmt.Printf("%T\n", Sum(1, 2, 3.0))

	// A generic function passed as an argument: its type arguments are inferred too (Go 1.21).
	fmt.Println(Apply(21, Double))
	fmt.Println(Apply(1.25, Double))

	// Assigning a generic function to a variable of a function type (Go 1.21 for
	// assignment to a variable, Go 1.27 in every context).
	var cmpInts func(a, b int) int = cmp.Compare
	words := []string{"Emma", "Dune", "Ulysses"}
	slices.SortFunc(words, strings.Compare)
	fmt.Println(cmpInts(1, 2), words)
}
int
float64
float64
42
2.5
-1 [Dune Emma Ulysses]
  • Before Go 1.21 Sum(1, 2.5) was an error ("mismatched types"). From 1.21 mixed untyped constants take the type that the same constants would get in an ordinary constant expression, which is float64 here.

  • Apply(21, Double) passes the generic function Double as an argument; since Go 1.21 its type arguments are inferred from the callee’s parameters.

  • Assigning cmp.Compare to a variable of type func(a, b int) int instantiates it with int. Go 1.27 generalised this so that inference works wherever a generic function meets a function type, such as conversions, return statements and arguments to non-generic functions:

package main

import (
	"cmp"
	"fmt"
	"strings"
)

func apply(f func(a, b string) int, x, y string) int { return f(x, y) }

func chooser(byLength bool) func(a, b string) int {
	if byLength {
		return func(a, b string) int { return cmp.Compare(len(a), len(b)) }
	}
	return cmp.Compare // type arguments inferred from the result type
}

func main() {
	// Conversion: the target function type determines T (all contexts in Go 1.27).
	g := (func(a, b float64) int)(cmp.Compare)
	// Argument to a non-generic function.
	fmt.Println(g(1.5, 0.5), apply(cmp.Compare, "Dune", "Emma"), apply(strings.Compare, "b", "a"))
	fmt.Println(chooser(false)("a", "b"), chooser(true)("aaa", "b"))
}
1 -1 1
-1 1

The release notes list "all contexts where a generic function is assigned to a variable of (or converted to) a matching function type". With go1.27.2 on macOS/arm64 I verified assignment, variable declarations, conversions, returns and arguments; a generic function used as an element of a composite literal (struct field, slice or map element) or sent on a channel made the compiler stop with an internal compiler error. If your toolchain does the same, instantiate explicitly (cmp.Compare[int]) and report it at https://go.dev/issue/new.

When inference fails

Inference never uses the destination of a call’s result (var x int = Zero() does not infer T), and it never infers the type arguments of a generic type, only of functions:

package main

func Zero[T any]() T { var z T; return z }

func main() {
	_ = Zero()
}
# example.com/shelf
./main.go:6:10: in call to Zero, cannot infer T (declared at ./main.go:3:11)
package main

import "cmp"

func MaxOf[T cmp.Ordered](a, b T) T {
	if a > b {
		return a
	}
	return b
}

type Book struct{ Title string }

func main() {
	_ = MaxOf(Book{"Dune"}, Book{"Emma"})
	_ = MaxOf(1, "two")
}
# example.com/shelf
./main.go:15:11: Book does not satisfy cmp.Ordered (Book missing in ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~float32 | ~float64 | ~string)
./main.go:16:15: in call to MaxOf, mismatched types untyped int and untyped string (cannot infer T)

The first message of the second program is the constraint check; the second shows that an untyped int and an untyped string constant cannot be unified into one T. Write the type argument explicitly (Zero[string]()) or give the compiler a typed argument.

Constraints and type sets

A type constraint is an interface. Methods in an interface say what a type must be able to do; type elements say what a type must be. The specification defines the resulting type set precisely (General interfaces):

  • the type set of the empty interface (any) is all non-interface types;

  • the type set of a non-empty interface is the intersection of its elements' type sets;

  • a method specification denotes all types whose method set includes the method;

  • a term T denotes just T; a term ~T denotes all types whose underlying type is T;

  • a union t1 | t2 | ... is the union of the terms' type sets.

flowchart TB subgraph ANY["any: the type set of every non-interface type"] subgraph CMP["comparable: types that support == and !="] subgraph ORD["cmp.Ordered: ~int, ~int8 ... ~uint ... ~float32, ~float64, ~string"] subgraph NUM["Number (user-defined): ~int, ~int64, ~float64"] INT["int, int64, float64 and defined types such as Pages"] end STR["string and defined types such as ISBN"] end OTH["bool, pointers, channels, interfaces, arrays and structs of comparable types"] end NC["slices, maps and functions: not comparable"] end

Each box is a subset of the box around it. A function constrained by any accepts everything but can do almost nothing with the value; each narrower constraint allows more operations on the values and accepts fewer types. ~int | ~string is the union of the two "all types with that underlying type" sets, so it includes both int and type ISBN string.

The predeclared and standard constraints

Constraint Type set Operations the body may use

any

every type (alias for interface{}, Go 1.18)

assignment, passing, var zero T, conversion to any; no ==

comparable

types whose values can be compared with == (see below)

== and !=; use as map key

cmp.Ordered

all integer, floating-point and string types and types derived from them (~int, …​, ~float64, ~string)

<, <=, >, >=, ==, !=; since Go 1.21 in the cmp package

~[]E, ~map[K]V, ~chan E

all slice, map or channel types with that element/key type

indexing, slicing, len, range, make, append, delete, channel operations

The old golang.org/x/exp/constraints package (Integer, Float, Ordered, …​) predates cmp.Ordered and the slices and maps packages; new code should use the standard library constraints or define small ones where it needs them.

Source: cmp package

Unions, approximation and method requirements

package main

import (
	"fmt"
	"slices"
)

type ISBN string
type Pages int

// Number is a union of approximation elements: any type whose underlying type
// is one of these satisfies it, including defined types such as Pages.
type Number interface {
	~int | ~int64 | ~float64
}

func Total[T Number](vs []T) T {
	var t T
	for _, v := range vs {
		t += v
	}
	return t
}

// Describer combines a method requirement with a type requirement.
type StringyInt interface {
	~int
	String() string
}

type State int

func (s State) String() string { return [...]string{"available", "on loan"}[s] }

func Label[T StringyInt](v T) string { return fmt.Sprintf("%d=%s", int(v), v.String()) }

// Index returns the position of v in s; comparable permits == and !=.
func Index[T comparable](s []T, v T) int {
	for i, e := range s {
		if e == v {
			return i
		}
	}
	return -1
}

func main() {
	fmt.Println(Total([]Pages{412, 1032}), Total([]float64{1.5, 2}))
	fmt.Println(Label(State(1)))
	fmt.Println(Index([]ISBN{"a", "b"}, "b"), slices.Index([]ISBN{"a", "b"}, "b"))
}
1444 3.5
1=on loan
1 1
  • ~int (the approximation or tilde element) accepts every type whose underlying type is int, such as type Pages int. Without the tilde, Total([]Pages{...}) would not compile because Pages is not int. The type after ~ must be its own underlying type: ~Pages is illegal.

  • A constraint may combine type elements and methods. StringyInt accepts only types that are both based on int and have a String() string method, so Label can use both int(v) and v.String().

  • Interfaces with type elements ("non-basic interfaces") can be used only as constraints, never as the type of a variable, field or parameter.

  • An inline constraint literal is allowed when it is short: [T int | string] is shorthand for [T interface{ int | string }].

The rules for unions

package main

import "fmt"

type MyInt int

type Overlap interface{ ~int | MyInt }

type Mixed interface{ int | fmt.Stringer }

type Float interface{ ~float32 | ~float64 }

var x Float

func main() {}
# example.com/shelf
./main.go:7:32: overlapping terms main.MyInt and ~int
./main.go:9:29: cannot use fmt.Stringer in union (fmt.Stringer contains methods)
./main.go:13:7: cannot use type Float outside a type constraint: interface contains type constraints

The rules are: terms of a union must have pairwise disjoint type sets (so ~int | MyInt is rejected); a union term cannot be an interface that has methods, and it cannot be comparable; and a type parameter cannot itself be a term. These restrictions are what let the compiler decide which operations are valid.

comparable and the Go 1.20 change

comparable is the constraint for ==, != and for map keys. Before Go 1.20 it was satisfied only by strictly comparable types: types whose comparison can never panic. Interfaces were excluded because comparing two interface values holding slices panics. Go 1.20 relaxed this: an interface type (and types containing interfaces) now satisfies comparable, and the comparison may panic at run time, exactly as it does for a map[any]int key.

package main

import (
	"fmt"
	"strings"
)

// Contains uses == on a comparable type parameter.
func Contains[T comparable](s []T, v T) bool {
	for _, e := range s {
		if e == v {
			return true
		}
	}
	return false
}

func try(name string, f func() bool) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Printf("%s: panic: %v\n", name, r)
		}
	}()
	fmt.Printf("%s: %t\n", name, f())
}

func main() {
	// Since Go 1.20 an interface type satisfies comparable, even though ==
	// on its dynamic values can panic.
	vals := []any{1, "Dune", 2.5}
	try("any slice", func() bool { return Contains(vals, "Dune") })

	// Comparing two uncomparable dynamic values panics at run time.
	bad := []any{[]int{1}, map[string]int{}}
	try("uncomparable", func() bool { return Contains(bad, any([]int{1})) })

	// Strictly comparable types never panic.
	try("strings", func() bool { return Contains(strings.Fields("a b c"), "b") })
}
any slice: true
uncomparable: panic: runtime error: comparing uncomparable type []int
strings: true

The panic message is the same one an ordinary == on those values would give. Use comparable for keys and membership tests, and remember that for any-typed elements the guarantee is dynamic. A module whose go line is below 1.20 reports any to satisfy comparable requires go1.20 or later.

Rejected programs

package main

import "fmt"

func Show[T any](v T) string {
	return v.String()
}

func Eq[T any](a, b T) bool { return a == b }

func Add[T any](a, b T) T { return a + b }

func main() { fmt.Println(Show(1), Eq(1, 2), Add(1, 2)) }
# example.com/shelf
./main.go:6:11: v.String undefined (type T has no field or method String)
./main.go:9:38: invalid operation: a == b (incomparable types in type set)
./main.go:11:36: invalid operation: operator + not defined on a (variable of type T constrained by any)

With any the body can do nothing but move values around: no method calls, no ==, no +. Add the capability to the constraint (fmt.Stringer, comparable, a union) and the error disappears. The complementary mistake is using a constraint as an ordinary type:

package main

type Number interface{ ~int | ~float64 }

var n Number

func main() { _ = n }
# example.com/shelf
./main.go:5:7: cannot use type Number outside a type constraint: interface contains type constraints
package main

type Sum interface{ ~int | ~string }

func Both[T Sum](v T) T { return v + v }

func Neg[T Sum](v T) T { return -v }

func Idx[T ~[]int | ~[]string](s T) { _ = s[0] }

func main() {}
# example.com/shelf
./main.go:7:34: invalid operation: operator - not defined on v (variable of type T constrained by Sum)
./main.go:9:44: cannot index s (variable of type T constrained by ~[]int | ~[]string)

For a union, an operator is allowed only if it is valid for every type in the type set (- is not defined on strings), and indexing needs the same element type for every member of the set. Constraint satisfaction errors name the missing method or the missing type: int does not satisfy fmt.Stringer (missing method String).

Choosing constraints in practice

  • Use the weakest constraint that lets the body compile: any before comparable before cmp.Ordered.

  • Prefer ~[]E over []E for slice-shaped parameters and results so that named slice types survive.

  • Define a constraint next to the function that needs it, not in a shared "constraints" package.

  • Keep method requirements small, the same advice as for interfaces; a constraint with ten methods is usually an interface in disguise.

Generic types

A type declaration can have type parameters. All methods of the type repeat the parameters in the receiver; they cannot add constraints of their own, and they use the receiver’s names (which may be renamed per method, or blank when unused).

package main

import (
	"fmt"
	"slices"
	"sort"
)

// Set is a generic set. The key type must be comparable so it can be a map key.
type Set[T comparable] map[T]struct{}

func NewSet[T comparable](items ...T) Set[T] {
	s := make(Set[T], len(items))
	for _, it := range items {
		s.Add(it)
	}
	return s
}

func (s Set[T]) Add(v T)      { s[v] = struct{}{} }
func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }
func (s Set[T]) Len() int     { return len(s) }
func (s Set[T]) Union(o Set[T]) Set[T] {
	out := NewSet[T]()
	for k := range s {
		out.Add(k)
	}
	for k := range o {
		out.Add(k)
	}
	return out
}

// Stack shows the zero value of a type parameter: var zero T.
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
}

// Pair has two type parameters.
type Pair[K comparable, V any] struct {
	Key K
	Val V
}

func (p Pair[K, V]) String() string { return fmt.Sprintf("%v=>%v", p.Key, p.Val) }

// SortedIndex keeps entries ordered by key, using binary search.
type SortedIndex[K interface{ ~string | ~int }, V any] struct {
	keys []K
	vals []V
}

func (x *SortedIndex[K, V]) Put(k K, v V) {
	i, found := slices.BinarySearch(x.keys, k)
	if found {
		x.vals[i] = v
		return
	}
	x.keys = slices.Insert(x.keys, i, k)
	x.vals = slices.Insert(x.vals, i, v)
}

func (x *SortedIndex[K, V]) Get(k K) (V, bool) {
	i, found := slices.BinarySearch(x.keys, k)
	if !found {
		var zero V
		return zero, false
	}
	return x.vals[i], true
}

func (x *SortedIndex[K, V]) Keys() []K { return slices.Clone(x.keys) }

func main() {
	a := NewSet("Dune", "Emma")
	b := NewSet("Emma", "Ulysses")
	u := a.Union(b)
	names := make([]string, 0, u.Len())
	for k := range u {
		names = append(names, k)
	}
	sort.Strings(names)
	fmt.Println(names, u.Has("Dune"), u.Has("Hamlet"))

	var st Stack[Pair[string, int]]
	st.Push(Pair[string, int]{"Dune", 412})
	st.Push(Pair[string, int]{"Emma", 474})
	top, _ := st.Pop()
	fmt.Println(top)
	_, _ = st.Pop()
	empty, ok := st.Pop()
	fmt.Printf("%v %t %q\n", empty, ok, empty.Key)

	var idx SortedIndex[string, int]
	for _, t := range []string{"Ulysses", "Dune", "Emma", "Dune"} {
		idx.Put(t, len(t))
	}
	v, _ := idx.Get("Emma")
	fmt.Println(idx.Keys(), v)
}
[Dune Emma Ulysses] true false
Emma=>474
=>0 false ""
[Dune Emma Ulysses] 4
  • Set[T comparable] is a map type. It needs comparable because T is a map key; the compiler checks the constraint every time Set[X] is written.

  • Stack[T any] has no constructor: the zero value is ready to use, as with sync.Mutex. Pop returns (zero, false) on empty, the idiomatic replacement for a nil result.

  • Pair[K comparable, V any] has two parameters; Pair[string, int]{"Dune", 412} shows that type arguments are required in composite literals, there is no inference for types.

  • SortedIndex uses an inline constraint interface{ ~string | ~int } and the generic slices.BinarySearch, which needs cmp.Ordered; this works because the union is a subset of it.

Rules and pitfalls for generic types

package main

type Stack[T any] struct{ items []T }

func (s Stack[T]) Len() int { return len(s.items) }

func main() {
	var s Stack
	_ = s
	var t Stack[int, string]
	_ = t
}
# example.com/shelf
./main.go:8:8: cannot use generic type Stack[T any] without instantiation
./main.go:10:8: too many type arguments for type Stack: have 2, want 1
package main

import "fmt"

type Pair[A, B any] struct {
	a A
	b B
}

func (p Pair[A, B]) Swap() Pair[B, A] { return Pair[B, A]{p.b, p.a} }

func (p Pair[A]) Bad() {}

func Show[T fmt.Stringer](v T) string { return v.String() }

func main() {
	_ = Show(3)
}
# example.com/shelf
./main.go:12:9: receiver declares 1 type parameter, but receiver base type declares 2
./main.go:17:10: int does not satisfy fmt.Stringer (missing method String)
package main

type Wrap[T any] struct{ T }

func Map[T, R any](s []T, f func(T) R) []R { return nil }

func main() {
	_ = Map[int, string, bool](nil, nil)
}
# example.com/shelf
./main.go:3:26: embedded field type cannot be a (pointer to a) type parameter
./main.go:8:23: got 3 type arguments but want 2
  • A generic type is not a type until instantiated; Stack alone is an error, and so is an instantiation with the wrong number of arguments.

  • The receiver must list all parameters of the type: func (p Pair[A]) Bad() is an error.

  • A struct cannot embed a type parameter, but it can embed an instantiated generic type (below).

  • In the receiver, the names in brackets are new names for the type’s parameters, not types. The following compiles, but int is a type parameter that shadows the predeclared type:

package main

import "fmt"

type Stack[T any] struct{ items []T }

// Pitfall: in the receiver, the names in brackets are NEW type parameters.
// Here "int" is a type parameter that shadows the predeclared type int.
func (s Stack[int]) Describe() string {
	var zero int // this is a T, whatever the instantiation is
	return fmt.Sprintf("%T", zero)
}

func main() {
	fmt.Println(Stack[string]{}.Describe())
	fmt.Println(Stack[float64]{}.Describe())
}
string
float64

Pointer-receiver constraints, blank receivers and embedding

Sometimes a generic function must create a T and call a method that has a pointer receiver. A constraint can tie T to its pointer type: PT interface{ *T; Method() }. Constraint type inference then deduces PT from T, so callers write only T.

package main

import (
	"errors"
	"fmt"
	"strings"
)

type ISBN string

// UnmarshalText has a pointer receiver, so only *ISBN has it.
func (i *ISBN) UnmarshalText(b []byte) error {
	s := strings.ReplaceAll(string(b), "-", "")
	if len(s) != 13 {
		return errors.New("catalog: invalid ISBN")
	}
	*i = ISBN(s)
	return nil
}

// Parse needs to create a T and call a pointer-receiver method on it.
// The constraint on PT says: PT is *T and it has UnmarshalText.
func Parse[T any, PT interface {
	*T
	UnmarshalText([]byte) error
}](s string) (T, error) {
	var v T
	err := PT(&v).UnmarshalText([]byte(s))
	return v, err
}

type Set[T comparable] map[T]struct{}

// The receiver type parameter may be blank when the method does not use it.
func (s Set[_]) Len() int { return len(s) }

func (s Set[T]) Add(v T) { s[v] = struct{}{} }

// Embedding an instantiated generic type is fine; the field is named Set.
type Tags struct {
	Set[string]
	Owner string
}

func main() {
	// PT is inferred from T by constraint type inference.
	isbn, err := Parse[ISBN]("978-0-441-17271-9")
	fmt.Println(isbn, err)
	_, err = Parse[ISBN]("123")
	fmt.Println(err)

	t := Tags{Set: Set[string]{}, Owner: "ann"}
	t.Add("scifi")
	t.Add("classic")
	fmt.Println(t.Len(), t.Owner)
}
9780441172719 <nil>
catalog: invalid ISBN
2 ann

Set[_] shows a blank receiver parameter for methods that do not use the type argument, and Tags shows that embedding Set[string] promotes Add and Len (the embedded field is named Set). See Structs and Embedding for the promotion rules.

Generic functions over slices, maps and channels

The standard library’s slices, maps and cmp packages (Go 1.21) are generic code that you rarely need to rewrite. The signatures show the idioms: func Sort[S ~[]E, E cmp.Ordered](x S), func Index[S ~[]E, E comparable](s S, v E) int, func SortFunc[S ~[]E, E any](x S, cmp func(a, b E) int), and func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K].

package main

import (
	"cmp"
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Book struct {
	Title string
	Year  int
}

// Filter returns the elements of s for which keep reports true.
func Filter[S ~[]E, E any](s S, keep func(E) bool) S {
	var out S
	for _, v := range s {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

// GroupBy partitions s by the key function.
func GroupBy[E any, K comparable](s []E, key func(E) K) map[K][]E {
	m := make(map[K][]E)
	for _, v := range s {
		k := key(v)
		m[k] = append(m[k], v)
	}
	return m
}

// Reduce folds s into a single value.
func Reduce[E, A any](s []E, init A, f func(A, E) A) A {
	acc := init
	for _, v := range s {
		acc = f(acc, v)
	}
	return acc
}

type Shelf []Book

func main() {
	books := Shelf{{"Dune", 1965}, {"Emma", 1815}, {"Ulysses", 1922}, {"Hamlet", 1603}}

	// The ~[]E form preserves the named slice type.
	modern := Filter(books, func(b Book) bool { return b.Year > 1900 })
	fmt.Printf("%T %v\n", modern, modern)

	byCentury := GroupBy(books, func(b Book) int { return b.Year / 100 * 100 })
	for _, c := range slices.Sorted(maps.Keys(byCentury)) {
		fmt.Println(c, byCentury[c])
	}

	chars := Reduce(books, 0, func(n int, b Book) int { return n + len(b.Title) })
	fmt.Println(chars)

	// The standard library versions.
	slices.SortFunc(books, func(a, b Book) int {
		return cmp.Or(cmp.Compare(a.Year, b.Year), strings.Compare(a.Title, b.Title))
	})
	fmt.Println(books)
	i, found := slices.BinarySearchFunc(books, 1922, func(b Book, year int) int { return cmp.Compare(b.Year, year) })
	fmt.Println(i, found)
	fmt.Println(slices.MaxFunc(books, func(a, b Book) int { return cmp.Compare(a.Year, b.Year) }))
	fmt.Println(slices.ContainsFunc(books, func(b Book) bool { return b.Title == "Emma" }))
}
main.Shelf [{Dune 1965} {Ulysses 1922}]
1600 [{Hamlet 1603}]
1800 [{Emma 1815}]
1900 [{Dune 1965} {Ulysses 1922}]
21
[{Hamlet 1603} {Emma 1815} {Ulysses 1922} {Dune 1965}]
2 true
{Dune 1965}
true

Filter has the S ~[]E signature, so filtering a Shelf returns a Shelf. GroupBy shows a second type parameter K comparable that is only used as a map key and is inferred from the function result. Reduce has an accumulator type A that is different from the element type. The last block uses the standard slices.SortFunc, BinarySearchFunc, MaxFunc and ContainsFunc, and cmp.Or to chain comparisons.

The same pattern works for maps (Keys[M ~map[K]V, K comparable, V any] in the earlier example) and for channels: func Merge[C ~chan E, E any](cs ...C) C accepts any channel type. Generic adapters on iterators are covered in Iterators and Range-over-Func; here is the idea:

package main

import (
	"fmt"
	"iter"
	"slices"
	"strings"
)

// FilterSeq and MapSeq are generic iterator adapters.
func FilterSeq[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] {
	return func(yield func(V) bool) {
		for v := range seq {
			if keep(v) && !yield(v) {
				return
			}
		}
	}
}

func MapSeq[V, R any](seq iter.Seq[V], f func(V) R) iter.Seq[R] {
	return func(yield func(R) bool) {
		for v := range seq {
			if !yield(f(v)) {
				return
			}
		}
	}
}

func main() {
	titles := []string{"Dune", "Emma", "Ulysses", "Hamlet"}
	long := FilterSeq(slices.Values(titles), func(s string) bool { return len(s) > 4 })
	upper := MapSeq(long, strings.ToUpper)
	fmt.Println(slices.Collect(upper))
}
[ULYSSES HAMLET]

Source: iter package

Generic APIs in the standard library

API Purpose Since

slices, maps, cmp

generic algorithms over slices and maps; three-way comparison

1.21

min, max, clear (builtins)

generic builtins over ordered types and maps/slices

1.21

sync/atomic.Pointer[T]

atomic pointer to a T without unsafe

1.19

sync.OnceValue, OnceValues

lazily computed values

1.21

iter.Seq[V], iter.Seq2[K, V], iter.Pull

iterator types and conversion

1.23

unique.Handle[T]

canonicalised comparable values

1.23

reflect.TypeFor[T], math/rand/v2.N

type or value without a dummy argument

1.22

weak.Pointer[T]

weak pointers

1.24

errors.AsType[T]

generic form of errors.As

1.26

(*rand.Rand).N (method)

first standard generic method

1.27

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"math/rand/v2"
	"sync/atomic"
	"time"
	"unique"
)

type Config struct{ Name string }

func main() {
	// math/rand/v2: N is a generic function, and in Go 1.27 also a generic method of *Rand.
	r := rand.New(rand.NewPCG(1, 2))
	fmt.Println(rand.N(time.Nanosecond) == 0, r.N(10) < 10, r.N(time.Hour) < time.Hour)

	// sync/atomic.Pointer[T] is a generic type (Go 1.19).
	var p atomic.Pointer[Config]
	p.Store(&Config{"shelf"})
	fmt.Println(p.Load().Name)

	// unique.Handle[T] canonicalises comparable values (Go 1.23).
	a, b := unique.Make("Dune"), unique.Make("Dune")
	fmt.Println(a == b, a.Value())

	// errors.AsType[T] is the generic form of errors.As (Go 1.26).
	err := fmt.Errorf("open: %w", &fs.PathError{Op: "open", Path: "x", Err: fs.ErrNotExist})
	pe, ok := errors.AsType[*fs.PathError](err)
	fmt.Println(ok, pe.Path)
}
true true true
shelf
true Dune
true x

Generic interfaces

An interface can have type parameters too. Repository[K, T] below is a store abstraction for any key and entity type, and Container[E] is used as a constraint tying a container type C to its element type E.

package main

import (
	"errors"
	"fmt"
	"sync"
)

// Repository is a generic interface: any entity type T with a key type K.
type Repository[K comparable, T any] interface {
	Get(K) (T, error)
	Put(K, T) error
}

var ErrNotFound = errors.New("not found")

// memRepo implements Repository[K, T] for every K and T.
type memRepo[K comparable, T any] struct {
	mu sync.Mutex
	m  map[K]T
}

func NewMemRepo[K comparable, T any]() *memRepo[K, T] {
	return &memRepo[K, T]{m: make(map[K]T)}
}

func (r *memRepo[K, T]) Get(k K) (T, error) {
	r.mu.Lock()
	defer r.mu.Unlock()
	v, ok := r.m[k]
	if !ok {
		var zero T
		return zero, fmt.Errorf("%v: %w", k, ErrNotFound)
	}
	return v, nil
}

func (r *memRepo[K, T]) Put(k K, v T) error {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.m[k] = v
	return nil
}

// Compile-time check that *memRepo[string, int] satisfies the instantiated interface.
var _ Repository[string, int] = (*memRepo[string, int])(nil)

type Book struct{ Title string }

// Lend accepts the interface, as in "accept interfaces, return structs".
func Lend(r Repository[string, Book], isbn string) (string, error) {
	b, err := r.Get(isbn)
	if err != nil {
		return "", err
	}
	return b.Title, nil
}

// Container is a generic interface used as a constraint: C must provide
// Len and At for the element type E.
type Container[E any] interface {
	Len() int
	At(int) E
}

type list[E any] []E

func (l list[E]) Len() int   { return len(l) }
func (l list[E]) At(i int) E { return l[i] }

func All[E any, C Container[E]](c C) []E {
	out := make([]E, 0, c.Len())
	for i := range c.Len() {
		out = append(out, c.At(i))
	}
	return out
}

func main() {
	r := NewMemRepo[string, Book]()
	_ = r.Put("9780441172719", Book{"Dune"})
	t, err := Lend(r, "9780441172719")
	fmt.Println(t, err)
	_, err = Lend(r, "0000000000000")
	fmt.Println(err, errors.Is(err, ErrNotFound))

	fmt.Println(All[string](list[string]{"x", "y"}))
}
Dune <nil>
0000000000000: not found true
[x y]
  • An instantiated generic interface is an ordinary interface: Repository[string, Book] can be a parameter type, and consumers accept it ("accept interfaces, return structs") while the constructor returns the concrete *memRepo[K, T].

  • The assertion var _ Repository[string, int] = (*memRepo[string, int])(nil) is the compile-time check for the instantiation you care about.

  • All[string](list[string]{...}) gives only E; C is inferred because a type argument must implement the constraint’s methods, and the compiler matches those methods against the argument’s (Go 1.21 added this method-based inference).

Generic type aliases (Go 1.24)

A type alias declares another name for a type. Go 1.24 lets an alias have type parameters. The alias is the same type, not a new one, so no conversion is needed and %T shows the original name.

package main

import "fmt"

type Set[T comparable] map[T]struct{}

// A generic alias: Index[V] is the same type as map[string]V.
type Index[V any] = map[string]V

// An alias may fix some arguments of a generic type: ISBNSet is Set[string].
type ISBNSet = Set[string]

// An alias may also instantiate a generic type partially.
type Result[T any] = struct {
	Val T
	Err error
}

func main() {
	var byISBN Index[int] = map[string]int{"9780441172719": 412}
	var plain map[string]int = byISBN // identical types: no conversion needed
	fmt.Printf("%T %v\n", byISBN, plain)

	s := ISBNSet{"9780441172719": {}}
	var t Set[string] = s
	fmt.Printf("%T %d\n", t, len(t))

	r := Result[int]{Val: 3}
	fmt.Printf("%T %+v\n", r, r)
}
map[string]int map[9780441172719:412]
main.Set[string] 1
struct { Val int; Err error } {Val:3 Err:<nil>}
  • type Index[V any] = map[string]V is a parameterised alias; Index[int] and map[string]int are identical.

  • type ISBNSet = Set[string] is an ordinary alias for an instantiated generic type. This already worked.

  • An alias is useful for evolving APIs: it lets a package move a generic type and keep the old name, and it shortens long instantiations. Go 1.24 enabled the feature by default; the GOEXPERIMENT=noaliastypeparams opt-out was announced to be removed in Go 1.25.

Self-referential type parameters (Go 1.26)

Go 1.18 forbade a type parameter list from referring to the type being declared, so a constraint could not say "a type that can combine with itself". Go 1.26 lifted this: type Adder[A Adder[A]] interface { Add(A) A } is legal, and it allows the F-bounded style that other languages call a "curiously recurring" or self-bounded type.

package main

import (
	"fmt"
	"strings"
)

// Adder may require that it be instantiated with a type like itself (Go 1.26).
type Adder[A Adder[A]] interface {
	Add(A) A
}

func Fold[A Adder[A]](zero A, vs ...A) A {
	acc := zero
	for _, v := range vs {
		acc = acc.Add(v)
	}
	return acc
}

// Pages adds page counts.
type Pages int

func (p Pages) Add(o Pages) Pages { return p + o }

// Title concatenates words.
type Title string

func (t Title) Add(o Title) Title { return Title(strings.TrimSpace(string(t) + " " + string(o))) }

// Ordered-style self reference: a value that can compare itself with another of its own type.
type Comparer[T Comparer[T]] interface {
	Compare(T) int
}

type Year int

func (y Year) Compare(o Year) int { return int(y - o) }

func Newest[T Comparer[T]](vs ...T) T {
	best := vs[0]
	for _, v := range vs[1:] {
		if v.Compare(best) > 0 {
			best = v
		}
	}
	return best
}

func main() {
	fmt.Println(Fold(Pages(0), 412, 1032, 96))
	fmt.Printf("%q\n", Fold(Title(""), "Dune", "Messiah"))
	fmt.Println(Newest(Year(1965), Year(1815), Year(1922)))
}
1540
"Dune Messiah"
1965

Fold works for any A whose Add method takes and returns A itself, with no any-typed parameters. Before 1.26 the same idea was written with an extra type parameter in the function (func Fold[A interface{ Add(A) A }](...)), which still works for functions but could not be expressed for a declared constraint interface.

Generic methods (Go 1.27)

Go 1.27 added generic methods: a method declaration can have its own type parameters in addition to those of its receiver. The motivation is organisation. A Map operation that changes the element type used to be a package-level function (MapList(l, f)), which crowds the package namespace and reads inside-out when chained. As a method it is local to the type and chains left to right.

package main

import (
	"fmt"
	"strconv"
)

type List[E any] struct{ elems []E }

func NewList[E any](elems ...E) List[E] { return List[E]{elems: elems} }

// Map is a generic method: it has its own type parameter R besides the receiver's E.
func (l List[E]) Map[R any](f func(E) R) List[R] {
	out := make([]R, 0, len(l.elems))
	for _, e := range l.elems {
		out = append(out, f(e))
	}
	return List[R]{elems: out}
}

func (l List[E]) String() string { return fmt.Sprint(l.elems) }

// Reduce shows two things: a method type parameter used in a result, and inference of R.
func (l List[E]) Reduce[A any](init A, f func(A, E) A) A {
	acc := init
	for _, e := range l.elems {
		acc = f(acc, e)
	}
	return acc
}

func main() {
	pages := NewList(412, 1032, 96)

	// R is inferred from the function argument; calls chain left to right.
	labels := pages.Map(strconv.Itoa).Map(func(s string) string { return s + "p" })
	fmt.Println(labels)

	// Explicit instantiation of the method.
	fmt.Println(pages.Map[float64](func(n int) float64 { return float64(n) / 2 }))

	// Reduce infers A from init.
	fmt.Println(pages.Reduce(0, func(a, n int) int { return a + n }))

	// A method expression turns the generic method into a plain function.
	f := List[int].Map[string]
	fmt.Println(f(pages, func(n int) string { return fmt.Sprintf("#%d", n) }))
}
[412p 1032p 96p]
[206 516 48]
1540
[#412 #1032 #96]
  • (l List[E]) Map[R any](...) declares R after the method name. E belongs to the receiver type, R to this method.

  • A generic method must be instantiated before it is used, like a generic function. The call pages.Map(strconv.Itoa) infers R; pages.Map[float64](...) gives it explicitly; Reduce infers A from init.

  • The method expression List[int].Map[string] yields an ordinary function of type func(List[int], func(int) string) List[string], which recovers the function-call form when you prefer it.

  • A method can also be generic when its receiver type is not: func (Empty) Nop[T any](x T) T.

  • The standard library uses it: (*rand.Rand).N[Int intType](Int) Int in math/rand/v2 is a generic method alongside the older generic function.

Generic methods cannot implement interfaces

Interface methods cannot declare type parameters, and a generic method cannot satisfy an interface method, even if one instantiation would have the right signature:

package main

type I interface {
	M[P any]()
}

type T struct{}

func (T) N[P any]() {}

func main() {}
# example.com/shelf
./main.go:4:3: interface method must have no type parameters
package main

type Box[T any] struct{ v T }

func (b Box[T]) Convert[U any]() U { var z U; return z }

type Conv interface{ Convert() int }

var _ Conv = Box[int]{}

func main() {}
# example.com/shelf
./main.go:9:14: cannot use Box[int]{} (value of struct type Box[int]) as Conv value in variable declaration: Box[int] does not implement Conv (wrong type for method Convert)
		have Convert[U any]() U
		want Convert() int

The reason is the way interface method calls are compiled. The compiler would have to generate code for every possible instantiation of every generic method of every type, because it cannot know which interface the value will be converted to. A dynamic assertion shows the same fact at run time:

package main

import "fmt"

type Empty struct{}

// Nop is a generic method: Empty has no method named Nop(string) string, only the generic Nop.
func (Empty) Nop[T any](x T) T { return x }

func TryNops(x any) {
	if x, ok := x.(interface{ Nop(string) string }); ok {
		fmt.Println("string", x.Nop("hello"))
	} else {
		fmt.Println("no Nop(string) string")
	}
}

func main() {
	fmt.Println(Empty{}.Nop("hello"), Empty{}.Nop[int](42))
	TryNops(Empty{})

	// An instantiated method value is an ordinary func value.
	f := Empty{}.Nop[string]
	fmt.Printf("%T %q\n", f, f("Dune"))
}
hello 42
no Nop(string) string
func(string) string "Dune"

Empty has the generic method Nop[T], but it has no method Nop(string) string, so the assertion fails. The Go FAQ entry "Why does Go not support methods with type parameters?" still says generic methods will never be added; that text describes the situation before 1.27, and the blog post and the release notes are authoritative.

The workaround when an interface is needed is a small non-generic wrapper that fixes the type arguments:

package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Book struct {
	Title string
	Year  int
}

// Store is a generic type; GroupBy is a generic method of it (Go 1.27).
type Store[T any] struct{ items []T }

func (s *Store[T]) Add(v T) { s.items = append(s.items, v) }

func (s *Store[T]) GroupBy[K comparable](key func(T) K) map[K][]T {
	m := make(map[K][]T)
	for _, v := range s.items {
		k := key(v)
		m[k] = append(m[k], v)
	}
	return m
}

// Grouper is an ordinary interface: its method has no type parameters.
type Grouper interface {
	ByDecade() map[int][]Book
}

// *Store[Book] satisfies Grouper through a non-generic wrapper, which fixes K.
type bookStore struct{ *Store[Book] }

func (b bookStore) ByDecade() map[int][]Book {
	return b.GroupBy(func(bk Book) int { return bk.Year / 10 * 10 })
}

var _ Grouper = bookStore{}

func main() {
	var s Store[Book]
	s.Add(Book{"Dune", 1965})
	s.Add(Book{"Neuromancer", 1984})
	s.Add(Book{"Emma", 1815})
	s.Add(Book{"Foundation", 1951})
	s.Add(Book{"Contact", 1985})

	byInitial := s.GroupBy(func(b Book) string { return strings.ToUpper(b.Title[:1]) })
	fmt.Println(len(byInitial), byInitial["D"])

	var g Grouper = bookStore{&s}
	dec := g.ByDecade()
	for _, d := range slices.Sorted(maps.Keys(dec)) {
		fmt.Println(d, dec[d])
	}
}
5 [{Dune 1965}]
1810 [{Emma 1815}]
1950 [{Foundation 1951}]
1960 [{Dune 1965}]
1980 [{Neuromancer 1984} {Contact 1985}]

GroupBy[K] is generic, so *Store[Book] cannot implement Grouper; the non-generic ByDecade of the wrapper can. The wrapper is the "adapter" idiom from Composition and Object-Oriented Design.

Two-column diagram. Left: a List[E] type with the generic method Map[R any](f func(E) R) List[R]; the call l.Map(f) infers R, l.Map[string](f) gives R explicitly, and the method expression List[int].Map[string] yields an ordinary function value. Right: an interface method with a type parameter is rejected by the compiler with the message interface method must have no type parameters, and Box[int] does not implement an interface whose Convert method has no type parameters because the method has the wrong type; the fix is a non-generic wrapper method that fixes the type arguments.

Version gate

A module whose go directive is below 1.27 cannot declare or use generic methods. The compiler says so:

# example.com/shelf
./main.go:13:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
./main.go:24:25: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)

Set go 1.27 in go.mod (see Toolchain Versions and Compatibility) before using the feature, and keep it in mind for libraries that still support Go 1.26.

Operations on type parameters and the removal of core types (Go 1.25)

When Go 1.18 added type parameters, the specification used an extra concept, the core type of a type set, to describe operations such as range, slicing, make and channel sends. It was "the single underlying type of all the types in the set". A constraint with different underlying types, such as ~[]byte | ~string, had no core type, and many operations were then forbidden or described by special cases. Go 1.25 removed core types from the specification and replaced them with explicit prose per operation. No program changed meaning, compiler messages became more specific, and the language is simpler to learn because non-generic code is described without generics terms.

What a body may do with a value x of type P, in terms of what the type set permits:

Operation Rule (spec: "if the operation is valid for every type in the type set…​")

x + y, -x, <, ==

valid for every type in the type set of P (- on strings is an error)

x[i], len(x), cap(x)

valid for all types in the set; for indexing the element types must be identical (byte for a string)

x[a:b]

valid if it is valid for every type in the set; with a mixed ~[]byte | ~string set the result has type P (Go 1.25 and later)

for range x

all types in the set must produce the same iteration values; a ~[]byte | ~string set is rejected (the compiler says the two have different underlying types)

make(P, n), append, copy, delete

each requires the set to agree (for example, all slices with the same element type)

c <- v, <-c, close(c)

all types must be channels of one element type, with a direction that permits the operation

P(x) conversions, T(p)

valid if the conversion is valid for each type in the set (a type parameter can be converted to any)

x.f (field selection)

not supported through a type set; use a method or a getter

package main

import "fmt"

// BytesOrString has no core type: its type set mixes []byte and string.
type BytesOrString interface{ ~[]byte | ~string }

// Indexing is valid for every type in the type set (and the element types agree).
func At[S BytesOrString](s S, i int) byte { return s[i] }

// Slicing a type set with differing underlying types: the spec rule is explicit
// prose since Go 1.25 (before, it was phrased through "core types").
func Head[S BytesOrString](s S, n int) S { return s[:n] }

// len works on any type whose type set agrees that len is valid.
func Size[S BytesOrString](s S) int { return len(s) }

// send needs every channel in the type set to allow sending.
func Send[C ~chan int | ~chan<- int](c C, v int) { c <- v }

// Conversions between type parameters work if all pairs are convertible.
func Bytes[S BytesOrString](s S) []byte { return []byte(s) }

func main() {
	fmt.Println(At("Dune", 1), At([]byte("Dune"), 1))
	fmt.Println(Head("Dune", 2), string(Head([]byte("Emma"), 3)))
	fmt.Println(Size("Ulysses"), Size([]byte("héllo")))
	c := make(chan int, 1)
	Send(c, 7)
	fmt.Println(<-c, Bytes("ab"))
}
117 117
Du Emm
7 6
7 [97 98]
package main

type BytesOrString interface{ ~[]byte | ~string }

func Count[S BytesOrString](s S) int {
	n := 0
	for range s {
		n++
	}
	return n
}

func main() {}
# example.com/shelf
./main.go:7:12: cannot range over s (variable of type S constrained by BytesOrString): []byte and string have different underlying types
package main

type XOnly interface{ ~struct{ X int } }

func GetX[T XOnly](v T) int { return v.X }

func main() {}
# example.com/shelf
./main.go:5:40: v.X undefined (type T has no field or method X)

The book-era phrase "core type" still appears in older articles and some tool messages; treat it as historical.

How generics are implemented

The language specification says nothing about the implementation, and the FAQ states that the compiler is free to choose between compiling each instantiation separately or sharing one implementation among similar instantiations (FAQ: How are generics implemented in Go?). The gc compiler uses a hybrid known as GC shape stenciling with dictionaries: instantiations whose type arguments have the same underlying representation ("GC shape") share one compiled body, and a hidden dictionary argument carries the type-specific operations. See the design notes for GC shape stenciling and dictionaries.

What this means for you:

  • Generic code is not free of cost in every case. Operations that depend on the type argument may go through the dictionary when instantiations share a body, so they can be slower than the same code written for a concrete type. Measure with a benchmark (b.Loop, see Performance and PGO) before writing type-specific copies "for speed".

  • Type arguments are visible at run time through reflect and %T.

  • Compile time and binary size grow with the number of distinct instantiations; they are not Java-style erased types.

  • The implementation may change between releases; the language rules do not depend on it.

When to use generics

The Go team’s guidance (The Go Blog: When To Use Generics) boils down to a handful of cases.

flowchart TD Q1{"Is the same algorithm needed for several element types?"} Q1 -- no --> I1["Write the concrete function"] Q1 -- yes --> Q2{"Does the body only call methods on the value?"} Q2 -- yes --> I2["Accept an interface: no type parameters needed"] Q2 -- no --> Q3{"Slices, maps, channels, containers, or operators on the values?"} Q3 -- yes --> G["Use type parameters with the weakest constraint"] Q3 -- no --> Q4{"Behaviour differs per type, or hot path needs specialisation?"} Q4 -- yes --> C["Write the variants by hand or use go generate"] Q4 -- no --> R["Reflection, only if the shape is genuinely dynamic"]
Situation Prefer Why

Functions over slices, maps and channels of any element type (Filter, Keys, Merge)

generics

the code is the same for every element type; slices and maps already provide many

General-purpose data structures (sets, stacks, trees, caches, pools)

generics

type-safe containers without any and type assertions

Operations that only need a behaviour (io.Reader, sort.Interface, http.Handler)

interfaces

an interface parameter says it all, and avoids instantiation overhead

Behaviour that differs per type

interfaces or a type switch

generic code must do the same thing for every type argument

Per-type specialisation, hot loops, special encoders

hand-written variants or go generate

see Directives, Code Generation and Embedding

Decoding unknown shapes (JSON, templates)

reflection

see Reflection; generics cannot inspect fields

A practical rule from the blog: write the concrete version first, and introduce type parameters when you are about to copy the function for a second type. Do not use generics to avoid a three-line for loop, and do not make a function generic just because its parameter is an interface.

Where the books are outdated

  • Bodner, Learning Go (1st ed.), chapter 15 "Generics" was written for Go 1.15. It presents generics through the draft design: an experimental playground (Go2Go), a type list syntax for constraints, and the Stack and Tree examples. None of that syntax shipped: the final language uses unions and ~T in ordinary interfaces, any, comparable, and later cmp.Ordered. The chapter also lists what generics leave out, including methods with type parameters; since Go 1.27 that limit is gone for concrete methods, while interface methods still cannot have type parameters.

  • Bodner, chapters 7 and 15: the guidance on interfaces versus generics still holds, but the examples use interface{}; write any, and use slices, maps and cmp instead of hand-written Map/Filter/Reduce.

  • Vivien, Learning Go Programming (2016) predates generics; it calls interface{} "generic". Translate those passages to any plus type assertions, or to type parameters where the code is a container or algorithm.

  • Effective Go carries its own note that it does not cover generics (it was written for the 2009 release), and the FAQ entry on generic methods is out of date as noted above.

Versions at a glance

Release Change

1.18

type parameters, any, comparable, type elements and ~T in interfaces, function and type instantiation, inference

1.20

interfaces satisfy comparable

1.21

cmp, slices, maps, min/max/clear; better inference: generic functions as arguments and in assignments, mixed untyped constants, method-based inference

1.22

cmp.Or, reflect.TypeFor, math/rand/v2.N

1.23

iterators (iter), unique.Handle[T]

1.24

generic type aliases enabled by default

1.25

core types removed from the specification (no change in behaviour)

1.26

self-referential type parameters; errors.AsType[T]

1.27

generic methods, e.g. (*rand.Rand).N; inference for generic functions in more contexts

Common mistakes

  • Declaring [T any] and then using ==, < or + on T: add comparable, cmp.Ordered or a union.

  • Forgetting ~ in a union and then wondering why Pages or ISBN is rejected.

  • Putting the type parameters that cannot be inferred last, forcing callers to repeat the ones that can be.

  • Writing return nil for T: use var zero T.

  • Reaching for generics when an interface expresses the behaviour (a function that only calls v.String() should take fmt.Stringer).

  • Using any-typed elements with comparable and getting a run-time panic (see the comparable example).

  • Shadowing a predeclared type by naming a receiver type parameter int or string.

  • Expecting a generic method to satisfy an interface method.

  • Assuming generics improve speed; they usually remove duplication, not allocations.