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 |
Type parameter |
A placeholder type declared by the list ( |
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 ( |
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 anydeclares two parameters with the same constraint, likea, b intfor 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))suppliesToand infersFromfrom the argument, which is whyTois 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
Tsupports exactly the operations that every type in the type set ofTsupports, nothing more. Withanyyou can assign, pass and return values, take their address and convert them toany.
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?").
Source: reflect: TypeFor
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:
-
Function argument type inference: typed arguments are matched against parameter types (
[]intagainst[]TgivesT = int;func(int) stringagainstfunc(T) RgivesR = string). -
Constraint type inference: a constraint with a single underlying type, such as
~[]E, connects parameters. GivenS = Counts, the compiler finds the underlying type[]Pagesand deducesE = Pages. -
Untyped constants: after typed arguments are processed, untyped constants supply a default type (
int,float64,rune,string, …). -
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.
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]
Source: The Go Programming Language Specification: Type inference and Go 1.21 release notes: Changes to the language
-
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 isfloat64here. -
Apply(21, Double)passes the generic functionDoubleas an argument; since Go 1.21 its type arguments are inferred from the callee’s parameters. -
Assigning
cmp.Compareto a variable of typefunc(a, b int) intinstantiates it withint. Go 1.27 generalised this so that inference works wherever a generic function meets a function type, such as conversions,returnstatements 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
Tdenotes justT; a term~Tdenotes all types whose underlying type isT; -
a union
t1 | t2 | ...is the union of the terms' type sets.
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 |
|---|---|---|
|
every type (alias for |
assignment, passing, |
|
types whose values can be compared with |
|
|
all integer, floating-point and string types and types derived from them ( |
|
|
all slice, map or channel types with that element/key type |
indexing, slicing, |
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 isint, such astype Pages int. Without the tilde,Total([]Pages{...})would not compile becausePagesis notint. The type after~must be its own underlying type:~Pagesis illegal. -
A constraint may combine type elements and methods.
StringyIntaccepts only types that are both based onintand have aString() stringmethod, soLabelcan use bothint(v)andv.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:
anybeforecomparablebeforecmp.Ordered. -
Prefer
~[]Eover[]Efor 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 needscomparablebecauseTis a map key; the compiler checks the constraint every timeSet[X]is written. -
Stack[T any]has no constructor: the zero value is ready to use, as withsync.Mutex.Popreturns(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. -
SortedIndexuses an inline constraintinterface{ ~string | ~int }and the genericslices.BinarySearch, which needscmp.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)
Source: The Go Programming Language Specification: Method declarations and Satisfying a type constraint
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;
Stackalone 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
intis 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 |
|---|---|---|
|
generic algorithms over slices and maps; three-way comparison |
1.21 |
|
generic builtins over ordered types and maps/slices |
1.21 |
|
atomic pointer to a |
1.19 |
|
lazily computed values |
1.21 |
|
iterator types and conversion |
1.23 |
|
canonicalised comparable values |
1.23 |
|
type or value without a dummy argument |
1.22 |
|
weak pointers |
1.24 |
|
generic form of |
1.26 |
|
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
Source: sync/atomic: Pointer
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 onlyE;Cis 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).
Source: The Go Blog: Generic interfaces
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]Vis a parameterised alias;Index[int]andmap[string]intare 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=noaliastypeparamsopt-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](...)declaresRafter the method name.Ebelongs to the receiver type,Rto this method. -
A generic method must be instantiated before it is used, like a generic function. The call
pages.Map(strconv.Itoa)infersR;pages.Map[float64](...)gives it explicitly;ReduceinfersAfrominit. -
The method expression
List[int].Map[string]yields an ordinary function of typefunc(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) Intinmath/rand/v2is a generic method alongside the older generic function.
Source: The Go Blog: Generic Methods
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
Source: The Go Programming Language Specification: Implementing an interface and The Go Blog: Generic Methods
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.
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…") |
|---|---|
|
valid for every type in the type set of |
|
valid for all types in the set; for indexing the element types must be identical (byte for a string) |
|
valid if it is valid for every type in the set; with a mixed |
|
all types in the set must produce the same iteration values; a |
|
each requires the set to agree (for example, all slices with the same element type) |
|
all types must be channels of one element type, with a direction that permits the operation |
|
valid if the conversion is valid for each type in the set (a type parameter can be converted to |
|
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]
Source: The Go Programming Language Specification: Index expressions, Slice expressions and Go 1.25 release notes: Changes to the language
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
reflectand%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.
| Situation | Prefer | Why |
|---|---|---|
Functions over slices, maps and channels of any element type ( |
generics |
the code is the same for every element type; |
General-purpose data structures (sets, stacks, trees, caches, pools) |
generics |
type-safe containers without |
Operations that only need a behaviour ( |
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 |
|
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
typelist syntax for constraints, and the Stack and Tree examples. None of that syntax shipped: the final language uses unions and~Tin ordinary interfaces,any,comparable, and latercmp.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{}; writeany, and useslices,mapsandcmpinstead of hand-writtenMap/Filter/Reduce. -
Vivien, Learning Go Programming (2016) predates generics; it calls
interface{}"generic". Translate those passages toanyplus 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, |
1.20 |
interfaces satisfy |
1.21 |
|
1.22 |
|
1.23 |
iterators ( |
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; |
1.27 |
generic methods, e.g. |
Common mistakes
-
Declaring
[T any]and then using==,<or+onT: addcomparable,cmp.Orderedor a union. -
Forgetting
~in a union and then wondering whyPagesorISBNis rejected. -
Putting the type parameters that cannot be inferred last, forcing callers to repeat the ones that can be.
-
Writing
return nilforT: usevar zero T. -
Reaching for generics when an interface expresses the behaviour (a function that only calls
v.String()should takefmt.Stringer). -
Using
any-typed elements withcomparableand getting a run-time panic (see thecomparableexample). -
Shadowing a predeclared type by naming a receiver type parameter
intorstring. -
Expecting a generic method to satisfy an interface method.
-
Assuming generics improve speed; they usually remove duplication, not allocations.
References
-
The Go Programming Language Specification: Type parameter declarations
-
The Go Programming Language Specification: General interfaces
-
The Go Programming Language Specification: Alias declarations
-
The Go Programming Language Specification: Method declarations
-
An Introduction To Generics, When To Use Generics, Type inference, All your comparable types, Robust generic functions on slices, Generic interfaces, What’s in an (Alias) Name?, Goodbye core types, Generic Methods