Skip to content
AgileLogics
8 min read

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 is int.

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 by any — it won't compile. Use comparable.
  • 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]
}