Generics
Prerequisites: Slices in depth
Generics (Go 1.18+) let you write a function or type once and use it with many types, while keeping full compile-time type checking. A function lists type parameters in square brackets, each with a constraint that says which types are allowed.
any— any type at all (but you can only do things every type supports).comparable— types that support==and!=, so they can be map keys.cmp.Ordered— types that support<,>and friends: integers, floats and strings.- Your own constraint: an interface listing allowed types with
|. A~prefix (~int) also allows types whose underlying type isint.
Syntax
func Map[T, U any](s []T, f func(T) U) []U { /* ... */ }
type Number interface {
~int | ~int64 | ~float64
}
type Stack[T any] struct {
items []T
}
Map[int, string](nums, f) // explicit type arguments
Map(nums, f) // usually inferred from the arguments
Example
package main
import (
"cmp"
"fmt"
)
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 MaxOf[T cmp.Ordered](first T, rest ...T) T {
m := first
for _, v := range rest {
if v > m {
m = v
}
}
return m
}
func IndexOf[T comparable](s []T, target T) int {
for i, v := range s {
if v == target {
return i
}
}
return -1
}
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
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
}
type Celsius float64 // underlying type float64, so it satisfies ~float64
func main() {
labels := Map([]int{1, 2, 3}, func(n int) string { return fmt.Sprint(n * 10) })
fmt.Println(labels)
fmt.Println(MaxOf(3, 7, 5), MaxOf("kiwi", "apple"))
fmt.Println(IndexOf([]string{"a", "b", "c"}, "c"))
fmt.Println(Sum([]Celsius{20.5, 1.5}))
var st Stack[string]
st.Push("first")
st.Push("second")
v, ok := st.Pop()
fmt.Println(v, ok)
st.Pop()
v, ok = st.Pop()
fmt.Printf("%q %v\n", v, ok)
}
Expected output
[10 20 30]
7 kiwi
2
22
second true
"" false
var zero T is the idiomatic way to get the zero value of a type parameter.
For simple cases, check the standard library first: the built-in min and
max functions and the slices and maps packages are all generic already.
Common mistakes
- Using
==on a type parameter constrained byany— it won't compile. Usecomparable. - Trying to give a method its own type parameters
(
func (s *Stack[T]) Map[U any]()). Methods can only use the type's parameters; write a top-level function instead. - Reaching for generics when an ordinary interface already expresses the behaviour. Generics shine for containers and algorithms over many types; interfaces shine for "anything that can do X".
Practice
Write Filter[T any](s []T, keep func(T) bool) []T. Use it to keep the even
numbers from 1..6 and the words longer than three letters from
[]string{"go", "rust", "zig", "python"}.
Show solution
package main
import "fmt"
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 main() {
evens := Filter([]int{1, 2, 3, 4, 5, 6}, func(n int) bool { return n%2 == 0 })
long := Filter([]string{"go", "rust", "zig", "python"}, func(s string) bool { return len(s) > 3 })
fmt.Println(evens) // [2 4 6]
fmt.Println(long) // [rust python]
}