Go Cheatsheet

Generics

Use this Go reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Type Parameters

Generics landed in Go 1.18. A function or type takes type parameters in square brackets before the value parameters.

// T is the type parameter, any is its constraint
func First[T any](s []T) (T, bool) {
    if len(s) == 0 {
        var zero T          // the only way to write "zero value of T"
        return zero, false
    }
    return s[0], true
}

n, ok := First([]int{1, 2, 3})       // T inferred as int
s, ok := First([]string{"a"})        // T inferred as string

// Several parameters, and one constraining another
func MapSlice[T, U any](in []T, f func(T) U) []U {
    out := make([]U, 0, len(in))
    for _, v := range in {
        out = append(out, f(v))
    }
    return out
}

lens := MapSlice([]string{"go", "rust"}, func(s string) int { return len(s) })

var zero T is the idiom for a generic zero value. You cannot write nil or 0, since T might be neither.

Constraints

A constraint is an interface used in a type-parameter list. On top of methods it may list type unions.

type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~float32 | ~float64
}

func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Inline, no named constraint needed
func Double[T ~int | ~float64](v T) T { return v * 2 }

The ~ operator means "any type whose underlying type is this", so named types satisfy the constraint too:

type Celsius float64

Double(Celsius(21))     // works with ~float64, fails with a bare float64
ConstraintMeaning
anyanything, alias for interface{}
comparableusable with == and !=, so valid as a map key
cmp.Orderedordered by <, <=, >, >= (1.21+, package cmp)
~Tany type with underlying type T
A | Bunion of terms
method seta normal interface, e.g. fmt.Stringer

golang.org/x/exp/constraints predates cmp and still offers Integer, Float, Signed, Unsigned and Complex. Its constraints.Ordered is superseded by cmp.Ordered in the standard library, so reach for cmp first and pull in x/exp only for the numeric families.

A union may not mix methods with type terms, and a type in a union cannot itself be an interface with methods.

Generic Types and Methods

type Stack[T any] []T

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

func (s *Stack[T]) Pop() (T, bool) {
    var zero T
    if len(*s) == 0 {
        return zero, false
    }
    old := *s
    v := old[len(old)-1]
    *s = old[:len(old)-1]
    return v, true
}

var s Stack[string]         // instantiation is explicit for types
s.Push("a")
v, ok := s.Pop()

// A generic struct with two parameters
type Cache[K comparable, V any] struct {
    mu sync.RWMutex
    m  map[K]V
}

func NewCache[K comparable, V any]() *Cache[K, V] {
    return &Cache[K, V]{m: make(map[K]V)}
}

func (c *Cache[K, V]) Get(k K) (V, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    v, ok := c.m[k]
    return v, ok
}

c := NewCache[string, int]()

Methods cannot declare their own type parameters. A method may only use the parameters of its receiver type. If you need a second type on a method, make it a package-level function.

// Illegal
// func (s *Stack[T]) MapTo[U any](f func(T) U) Stack[U] { ... }

// Legal
func MapStack[T, U any](s Stack[T], f func(T) U) Stack[U] { /* ... */ }

Type Inference

Inference works from the value arguments, never from the return type or the assignment target.

First([]int{1})              // inferred
MapSlice(xs, f)              // inferred from both args

// A type parameter that appears only in the result must be written out
func Zero[T any]() T { var z T; return z }
z := Zero[int]()             // Zero() does not compile

// Same when the arg is an untyped nil or an empty literal
func Keys[K comparable, V any](m map[K]V) []K { /* ... */ }
Keys(map[string]int{})       // fine
Keys[string, int](nil)       // nil alone gives inference nothing to work with

// Partial instantiation is allowed, left to right
func Convert[T, U any](in []T) []U { /* ... */ }
Convert[int, string](xs)     // both, U cannot be inferred

Generic types are always instantiated explicitly (Stack[int]), only generic functions infer. Assigning a generic function to a variable also requires instantiating it first: f := First[int].

Standard Library Generics (1.21+)

The builtins min, max and clear arrived in 1.21 and need no import.

min(3, 1, 2)                 // 1, works on any ordered type
max(2.5, 7.1)                // 7.1
clear(m)                     // empty a map, or zero every slice element

slices and maps are the generic workhorses:

import ("cmp"; "maps"; "slices")

slices.Sort(xs)                                   // ordered types
slices.SortFunc(people, func(a, b Person) int {   // -1, 0, +1
    return cmp.Compare(a.Age, b.Age)
})
slices.SortStableFunc(people, byAge)
slices.Contains(xs, 3)                            // bool
slices.Index(xs, 3)                               // int, -1 if absent
slices.IndexFunc(xs, func(v int) bool { return v > 10 })
i, found := slices.BinarySearch(sorted, 7)        // sorted input required
xs = slices.Insert(xs, 1, 9, 8)                   // insert at index 1
xs = slices.Delete(xs, 1, 3)                      // drop [1,3)
xs = slices.DeleteFunc(xs, func(v int) bool { return v < 0 })
slices.Equal(a, b)
slices.Reverse(xs)
xs = slices.Compact(xs)                           // dedupe adjacent
lo, hi := slices.Min(xs), slices.Max(xs)
ys := slices.Clone(xs)
cmp.Compare(a, b)                                 // -1 / 0 / +1
cmp.Or(a, b, c)                                   // first non-zero value

slices.Insert, Delete, Compact and DeleteFunc return a slice and may reuse the backing array, so always reassign and never keep using the old header.

Iterators (1.23+)

iter.Seq[V] is func(yield func(V) bool), and range accepts one directly. maps.Keys and maps.Values return iterators, not slices.

for k := range maps.Keys(m) { /* ... */ }            // iter.Seq[K]
for k, v := range maps.All(m) { /* ... */ }          // iter.Seq2[K, V]
keys := slices.Sorted(maps.Keys(m))                  // []K, sorted
vals := slices.Collect(maps.Values(m))               // []V
for i, v := range slices.Backward(xs) { /* ... */ }  // reverse with index

// Writing your own: yield returns false when the caller breaks
func Filter[T any](s []T, keep func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for _, v := range s {
            if keep(v) && !yield(v) {
                return
            }
        }
    }
}

for v := range Filter(xs, func(v int) bool { return v%2 == 0 }) {
    fmt.Println(v)
}

x/exp/maps.Keys returned a []K, the stdlib maps.Keys returns a sequence. Mixing them up is the most common upgrade error. Use slices.Collect or slices.Sorted to get a slice back.

When Not to Use Generics

Generics are for algorithms over containers, not for abstraction in general.

- If the function only calls methods on its argument, take an interface. That is what interfaces are for, and it compiles smaller and reads better. - Generics do not help you write polymorphic methods, since a method cannot add type parameters. - One implementation, one call site, one type: write the concrete function. - A constraint with a long type union usually means the operation belongs on a method instead. - any in a signature that then type-switches is not generics, it is a type switch with extra steps.

The Go team's own guidance: write the concrete code first, then factor out a type parameter when you have written the same body two or three times for different types.