Skip to main content
Go · lesson 20 of 25
Go advanced Lesson 20 of 25

Generics in Go

Go generics run for real: type parameters, inference, comparable and cmp.Ordered, ~ constraints, generic types, generic methods (Go 1.27), and iter.Seq.

By SkillByExample · Updated

Generics let one function or type work over many element types while staying fully type-checked. You declare type parameters in square brackets and a constraint that says what the code may do with them. Every example is a runnable program.

A generic function

Without generics you write SumInts and SumFloats separately. With them, one function covers both:

package main

import "fmt"

type Number interface {
	~int | ~int64 | ~float64
}

func Sum[T Number](nums []T) T {
	var total T // zero value of whatever T is
	for _, n := range nums {
		total += n
	}
	return total
}

func main() {
	fmt.Println(Sum([]int{1, 2, 3}))
	fmt.Println(Sum([]float64{1.5, 2.25}))
	fmt.Println(Sum[int64](nil))
}
$ go run sum.go
6
3.75
0

[T Number] declares a type parameter T constrained by Number, a union of types that support +. Go infers T from the argument in the first two calls; the third passes nil, which carries no element type, so it names T explicitly. var total T is how generic code gets a zero value.

Constraints decide what the body may do

The body can only use operations every permitted type supports. any permits every type, so it permits almost no operations:

package main

import "fmt"

func Max[T any](a, b T) T {
	if a > b {
		return a
	}
	return b
}

func main() { fmt.Println(Max(3, 7)) }
$ go run anyerr.go
# command-line-arguments
./anyerr.go:6:5: invalid operation: a > b (type parameter T cannot use operator >)

> isn’t defined for every type (structs, maps), so the compiler rejects it — at the definition, not at some call site. cmp.Ordered from the standard library is the constraint for “supports <”; comparable is the one for “supports ==”:

package main

import (
	"cmp"
	"fmt"
)

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

func Index[T comparable](s []T, v T) int {
	for i, x := range s {
		if x == v {
			return i
		}
	}
	return -1
}

type Point struct{ X, Y int }

func main() {
	fmt.Println(Max(3, 7), Max("apple", "pear"), Max(2.5, -1.0))
	fmt.Println(Index([]Point{{1, 2}, {3, 4}}, Point{3, 4}))
	fmt.Println(Index([]string{"a", "b"}, "z"))
}
$ go run ordered.go
7 pear 2.5
1
-1

Point isn’t ordered, but it is comparable, so it works with Index. The standard library already ships most of these helpers — slices.Index, slices.Max, and the built-in max — so check there before writing your own.

The ~ in a constraint

~int means “any type whose underlying type is int”. Without the tilde, named types are excluded.

package main

import "fmt"

type Celsius int

type Number interface{ ~int | ~float64 }

func Double[T Number](v T) T { return v * 2 }

func main() {
	d := Double(Celsius(21))
	fmt.Printf("%v %T\n", d, d)
}
$ go run tilde.go
42 main.Celsius

Celsius is a distinct type whose underlying type is int, so ~int admits it — and Double returned a Celsius, not an int: generics preserve the caller’s type. Drop the tildes and the same call is rejected:

package main

import "fmt"

type Celsius int

type Exact interface{ int | float64 }

func Triple[T Exact](v T) T { return v * 3 }

func main() {
	fmt.Println(Triple(Celsius(21)))
}
$ go run tildeerr.go
# command-line-arguments
./tildeerr.go:12:20: Celsius does not satisfy Exact (possibly missing ~ for int in Exact)

Exact lists only int itself, and the compiler even suggests the missing ~. Library constraints such as cmp.Ordered use ~ throughout so they work with your own defined types.

Generic types

Types can have type parameters too. Methods use the receiver’s parameters.

package main

import "fmt"

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
}

func main() {
	var s Stack[string]
	s.Push("a")
	s.Push("b")
	fmt.Println(s.Pop())
	fmt.Println(s.Pop())
	fmt.Println(s.Pop())
	fmt.Printf("%T\n", s)
}
$ go run stack.go
b true
a true
 false
main.Stack[string]

Stack[string] is an instantiation: a concrete type where every T is string. Popping an empty stack returns the zero value and false, the comma-ok convention.

Methods with their own type parameters (Go 1.27)

Since Go 1.27, a method can declare type parameters of its own, next to the ones it gets from the receiver. That lets Map change the element type of the stack it is called on:

package main

import (
	"fmt"
	"strconv"
)

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

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

// Map declares its own type parameter U, on top of the receiver's T.
func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] {
	out := &Stack[U]{}
	for _, v := range s.items {
		out.Push(f(v))
	}
	return out
}

func main() {
	var s Stack[int]
	s.Push(1)
	s.Push(2)
	labels := s.Map(func(n int) string { return "#" + strconv.Itoa(n) })
	fmt.Println(labels.items)
	fmt.Printf("%T\n", labels)
}
$ go version
go version go1.27.2 linux/amd64
$ go run genmethod.go
[#1 #2]
*main.Stack[string]

T is int, fixed when s was declared. U is inferred as string from the function literal at the call, just as for a generic function. The same file on Go 1.26 doesn’t parse:

$ go version
go version go1.26.9 linux/amd64
$ go run genmethod.go
# command-line-arguments
./genmethod.go:15:23: syntax error: method must have no type parameters

From Go 1.18 to 1.26, methods could only use their receiver’s type parameters, so code like this was written as a top-level function, such as the Map in the next section. You’ll still see that style everywhere. The 1.27 toolchain also checks the go line in go.mod. Here is the same file in a module that still says go 1.26:

$ go version
go version go1.27.2 linux/amd64
$ cat go.mod
module example.com/gm

go 1.26
$ go run .
# example.com/gm
./genmethod.go:15:24: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)

Raising the line to go 1.27 (go mod edit -go=1.27) fixes it. One limit remains: an interface method still can’t have type parameters, so a generic method can’t be used to satisfy an interface.

Map, Filter and Reduce

Functions with two type parameters can transform one element type into another.

package main

import (
	"fmt"
	"strconv"
)

func Map[T, U any](s []T, f func(T) U) []U {
	out := make([]U, 0, len(s))
	for _, v := range s {
		out = append(out, f(v))
	}
	return out
}

func Filter[T any](s []T, keep func(T) bool) []T {
	var out []T
	for _, v := range s {
		if keep(v) {
			out = append(out, v)
		}
	}
	return out
}

func Reduce[T, A any](s []T, init A, f func(A, T) A) A {
	acc := init
	for _, v := range s {
		acc = f(acc, v)
	}
	return acc
}

func main() {
	nums := []int{1, 2, 3, 4, 5, 6}
	evens := Filter(nums, func(n int) bool { return n%2 == 0 })
	labels := Map(evens, func(n int) string { return "#" + strconv.Itoa(n) })
	total := Reduce(nums, 0.0, func(acc float64, n int) float64 { return acc + float64(n)/2 })
	fmt.Println(evens, labels, total)
}
$ go run mapfilter.go
[2 4 6] [#2 #4 #6] 10.5

Map infers T = int and U = string from its arguments; Reduce infers the accumulator type float64 from 0.0. Writing 0 there would make A an int, and the function literal’s float64 signature would then fail to compile.

Iterators with iter.Seq

Since Go 1.23, a function of type iter.Seq[T] can be ranged over directly — the generic way to expose a sequence without building a slice.

package main

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

func Evens(limit int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for n := 0; n < limit; n += 2 {
			if !yield(n) { // the loop body returned or broke
				return
			}
		}
	}
}

func main() {
	for n := range Evens(100) {
		if n > 6 {
			break
		}
		fmt.Print(n, " ")
	}
	fmt.Println()

	fmt.Println(slices.Collect(Evens(10)))
	ages := map[string]int{"bo": 31, "al": 25}
	fmt.Println(slices.Sorted(maps.Keys(ages)))
}
$ go run iter.go
0 2 4 6
[0 2 4 6 8]
[al bo]

The range loop calls Evens(100)’s function, and each yield runs one iteration of the loop body. When the body breaks, yield returns false and the iterator must stop. slices.Collect and maps.Keys are generic helpers built on the same types.

Practice

1. Write Keys[K comparable, V any](m map[K]V) []K and use it with slices.Sort on a map[string]int.

Solution
package main

import (
	"fmt"
	"slices"
)

func Keys[K comparable, V any](m map[K]V) []K {
	out := make([]K, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	return out
}

func main() {
	ks := Keys(map[string]int{"b": 2, "a": 1, "c": 3})
	slices.Sort(ks)
	fmt.Println(ks)
}
$ go run p1.go
[a b c]

Map keys must be comparable, so K needs that constraint; V can be anything. The standard version is slices.Collect(maps.Keys(m)).

2. Write a generic Set[T comparable] type with Add, Has and Len methods.

Solution
package main

import "fmt"

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

func NewSet[T comparable](items ...T) *Set[T] {
	s := &Set[T]{m: map[T]struct{}{}}
	for _, it := range items {
		s.Add(it)
	}
	return s
}

func (s *Set[T]) Add(v T)      { s.m[v] = struct{}{} }
func (s *Set[T]) Has(v T) bool { _, ok := s.m[v]; return ok }
func (s *Set[T]) Len() int     { return len(s.m) }

func main() {
	s := NewSet(3, 1, 3, 2)
	fmt.Println(s.Len(), s.Has(2), s.Has(9))
}
$ go run p2.go
3 true false

NewSet(3, 1, 3, 2) infers T = int; the duplicate 3 is stored once.

3. Write MinBy[T any](s []T, less func(a, b T) bool) (T, bool) and find the shortest string in []string{"kiwi", "fig", "banana"}.

Solution
package main

import "fmt"

func MinBy[T any](s []T, less func(a, b T) bool) (T, bool) {
	var zero T
	if len(s) == 0 {
		return zero, false
	}
	m := s[0]
	for _, v := range s[1:] {
		if less(v, m) {
			m = v
		}
	}
	return m, true
}

func main() {
	fmt.Println(MinBy([]string{"kiwi", "fig", "banana"}, func(a, b string) bool { return len(a) < len(b) }))
	fmt.Println(MinBy([]int{}, func(a, b int) bool { return a < b }))
}
$ go run p3.go
fig true
0 false

Passing the comparison as a function lets T stay any, so it works for types that aren’t ordered at all.

4. Write an iterator Countdown(n int) iter.Seq[int] that yields n down to 1, and range over it.

Solution
package main

import (
	"fmt"
	"iter"
)

func Countdown(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := n; i >= 1; i-- {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	for i := range Countdown(5) {
		fmt.Print(i, " ")
	}
	fmt.Println("liftoff")
}
$ go run p4.go
5 4 3 2 1 liftoff

Checking yield’s result matters: without it, a break in the caller’s loop would make the runtime panic when the iterator calls yield again.

Frequently Asked Questions

When were generics added to Go?
Type parameters arrived in Go 1.18 in March 2022. Later releases built on them: the slices, maps and cmp packages in Go 1.21, range-over-function iterators with the iter package in Go 1.23, and methods with their own type parameters in Go 1.27.
What is a type constraint?
A constraint is an interface that says which types a type parameter accepts and therefore which operations the generic code may use. any allows every type, comparable allows types that support ==, cmp.Ordered allows types that support <, and you can write unions such as ~int | ~float64.
What does the tilde (~) mean in a Go constraint?
~int means any type whose underlying type is int, including named types such as type Celsius int. Without the tilde, the constraint int matches only int itself, so a defined type built on int would be rejected at compile time.
Can Go methods have their own type parameters?
Yes, since Go 1.27. A method can declare its own type parameters next to its receiver's, for example func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U]. From Go 1.18 to 1.26 that was a syntax error, so older code uses top-level functions such as func Map[T, U any](s []T, f func(T) U) []U. Interface methods still cannot have type parameters.
Should I rewrite my code with generics?
Only where the same logic is duplicated across element types, which mostly means containers and helpers over slices, maps and channels. If a function would work on one concrete type, or an interface with methods already expresses the behaviour, keep it non-generic; it is simpler to read.