/ website / blog

go generics for the busy gopher

February 6, 2022

What is this?

(!) caution: Language pedants may find the content triggering

new terms

generics: The idea that type information can be determined not during implementation, but later

type parameter: The thing that represents potential types during implementation

constraint: The thing that places restrictions on a type parameter

the min example

Remember writing this over and over?

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

Of course that only works for type int. You’d have to implement the same function for int64, float64, uint8, … etc. Well generics can fix this. So let’s fix it.

We’ll need to add a type parameter to make the function generic. Which type parameter? Well, one that lets us do the things we need to do with the type. We describe such a thing as a constraint. In this case we need to be able to use the < operator. Luckily, Go comes with just the right tool for the job, constraints.Ordered.

(!) caution: It seems the constraints package may be moved under exp/ for Go 1.18 final, indicating it is still experimental. Use at your own peril.

Okay lets add that.

func min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

Awesome! Our min function is now generic and will work for any type that satisfies the Ordered type constraint. We’ll talk about custom constraints later.

the set example

Making a function generic is nice, but what about creating a generic data structure?

Let’s create a trivial set implementation that lets us add items. Basically a re-usable wrapper for the conventional abuse of map[<type>]struct{}. We can then add some functions for basic set operations.

First we declare our parameterized type. Should we use any, the drop-in replacement for interface{}? No, because any does not allow use to use the == and != operators, which we’ll need for comparing items in the set. Instead, we use the built-in comparable constraint.

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

Fantastic, we now have a generic Set type. Let’s add a constructor so we can safely instantiate that underlying map. Notice how the NewSet function is parameterized with the generic type parameter we want, and that type information is used when instantiating the Set.

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{
        m: make(map[T]struct{}),
    }
}

Now that we have a generic data structure, lets add some methods to make it useful. Most sets contain elements, so how about an Insert method. Because our Set is generic for [T comparable] we should parameterize our Insert method to accept only elements of type T accordingly.

func (s *Set[T]) Insert(elem T) {
    s.m[elem] = struct{}{}
}

That’s it! You can imagine implementing all the other usual set operations in a similar fashion. Do not be afraid of using Set[T] as a method parameter type or return type, it all works the same. This might be the signature for a Union method, for example.

func (s *Set[T]) Union(o *Set[T]) *Set[T] {
    // ...
}

the custom constraint example

So far we have only used constraint types offered by Go (constraints.Ordered and comparable). What if we want a custom constraint? Like if we want something that only accepts types that implement a Pretty() string function. For that we could create our custom constraint,

type Prettier interface {
    Pretty() string
}

Now we can use our interface as a type constraint for type parameters on our generic functions or structs. Here is a PrettyPrint function that accepts a slice of Prettier, calling Pretty on each element. (Unlike before generics, when we would have to create a new slice of type []Prettier, convert each element, etc…)

func PrettyPrint[T Prettier](items []T) {
    for _, item := range items {
        fmt.Println(item.Pretty())
    }
}

more custom constraint syntax

Constraints can be used to describe the union of more than one type. For example, we might want a constraint allowing only the 8-bit integer types byte, int8, and uint8. To do this we use the | (union) operator.

type ByteSize interface {
    byte | uint8 | int8
}

constraints can be generic

Constraints themselves can be declared generically.

type Bar interface {
    Bar()
}

type Foo[B Bar] interface {
    Foo[B]()
}

TBH I am not sure what to do with that.

the end

Congrats, you now know enough about Go generics to write some reusable code. But please remember, with great power comes great responsibility. I do not hope to see Go2EE become a thing.

Happy hacking!

➡️ related posts in the go-code series ...