Go · 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.