Early Perspectives on Go Generics

matthewdale

Matt Dale

Posted on March 1, 2022

Early Perspectives on Go Generics

Go 1.18 is scheduled for release sometime soon (originally Feb 2022) and will be the first version of Go to support generic programming, commonly called "generics". The lack of generics in Go has been a contentious topic for a long time, as Ian Lance Taylor pointed out in his GopherCon presentation in 2019:

Go was released November 10, 2009. Less than 24 hours later, we saw the first comment about generics.
— Ian Lance Taylor, GopherCon 2019: Generics in Go

There are already lots of good articles, tutorials, and talks about generics in Go, so this article will just focus on the experience of writing and using generic code in Go.


Disclaimer: Go 1.18 is not released yet! These early perspectives on Go generics are using the latest gotip version as of Feb 21, 2022. Some of the behaviors described in this article may change in the final release.


What's the Big Deal with Generics?

Before we get into what using generics in Go is like, let's take a step back and ask, "Why do developers use generics and what improvements do generics offer?" Let's turn to a quote by Alexander Stepanov, original designer of the C++ Standard Template Library, for help:

Generic programming is about abstracting and classifying algorithms and data structures ... Its goal is the incremental construction of systematic catalogs of useful, efficient and abstract algorithms and data structures.
— Alexander Stepanov, Short History of STL

So generic programming is primarily about being able to focus on writing code once and using it many times with many types. How does it work in Go?

Go Generics: The Really Really Short Version

Go generics adds a "type constraint" syntax that allows functions and types to specify multiple types that they can receive and return. Let's make that more concrete with a simple Min function that accepts two numeric values and returns the minimum value of the two. Our Min function can accept two parameters, both either an int64 or float64, and returns a value the same type as the inputs.

func Min[T int64 | float64](x, y T) T {
    if x < y {
        return x
    }
    return y
}
Enter fullscreen mode Exit fullscreen mode

To call our generic Min function, we call it with values that satisfy the type constraint, int64 in this case. The Go compiler infers appropriate types, resulting in code that looks the same as non-generic Go code.

var x int64 = 5
var y int64 = 12
Min(x, y) // returns 5
Enter fullscreen mode Exit fullscreen mode

For a more in-depth look at generics in Go, check out the (beta) Go generics tutorial.

Early Perspectives

Here's what you came for, some selected early perspectives on using Go generics!

Generic Type Inference

Type inference is a concept already familiar to most Go developers. When you assign a value to a variable using the := assignment operator, you're asking the Go compiler to infer the correct variable type. Go generics use a similar concept to try to determine the correct types to use when calling a generic function.

The good news is type inference works great with function parameters! Calling functions that use generic input parameter types is very intuitive in most cases, and is often syntactically identical to calling non-generic functions.

However, calling functions with a generic return type often requires you to specify the type in the call. For example, let's consider the following generic function that returns a positive infinity floating point value as a float32 or float64:

func Inf[T float32 | float64](t) T {
    return T(math.Inf(1))
}
Enter fullscreen mode Exit fullscreen mode

Now let's try to call the Inf function to assign a +Inf value to a float64 variable:

var x float64
x = Inf()
Enter fullscreen mode Exit fullscreen mode

Oops, we got a compile error!

./main.go:20:15: cannot infer T (./main.go:10:10)
Enter fullscreen mode Exit fullscreen mode

Instead, we have to specify the function type explicitly:

var x float64
x = Inf[float64]()
Enter fullscreen mode Exit fullscreen mode

As a result, calling generic functions where the Go compiler can't use the input parameters to infer the return type can be somewhat awkward. There's an open discussion about whether or not Go should support generic function type inference when assigning to a variable here, but for now you have to declare the return type explicitly.

APIs and Type Switches

Generics also gives us new options when we want to add functionality to APIs while maintaining backward compatibility. Consider a function Do that takes an int value and performs some action.

func Do(x int) {
    // Perform some action with int x.
}
Enter fullscreen mode Exit fullscreen mode

Now let's say we want Do to support either an int or a bool, and perform a different action for with each type. Before generics, we might have just added new functions DoInt and DoBool, and kept the Do function to maintain API backward compatibility.

func DoInt(x int) {
    // Perform some action with int x.
}

func DoBool(x bool) {
    // Perform some action with bool x.
}

func Do(x int) {
    DoInt(x)
}
Enter fullscreen mode Exit fullscreen mode

With generics, we can add a type constraint to our Do function to accept either an int or a bool and use a type switch to do the appropriate action for each type.

func Do[T int | bool](x T) {
    switch any(x).(type) {
    case int:
        // Perform some action with int x.
    case bool:
        // Perform some action with bool x.
    }
}
Enter fullscreen mode Exit fullscreen mode

Note: any is a new shorthand for interface{}.

We've reduced our API surface area by only using a single Do function, and maintained API backward compatibility in most cases (calls to Do using reflection may still be impacted), but the resulting implementation code is less clear than two separate functions. Additionally, there's no compile-time check to ensure that the type constraint and switch cases match, so they may get out-of-sync and lead to bugs.

Whether or not it's a good idea to update APIs to accept more types using type constraints is still unclear. There is an open discussion about amending the type switch to work more gracefully with type parameters, so follow that if you're interested in the outcome!

Testing

With more types come more tests! If your functions only support a single type, then you only need to test with values of a single type. Once your functions support many types, you need to test with values of every supported type. That can balloon into lots of test cases quickly!

To complicate things, some common testing practices won't work when using generics. For example, anonymous functions and closures cannot use type parameters (check out the discussion here).

As a result, you may end up with quite large tests with lots of subtests and lots of test cases, like the ones here. We'll need to develop new testing best practices to handle the new challenges that come with testing generic code in Go.

Wrapping Up

Go generics will be available soon and may significantly change how we write Go. There is a lot to learn about the best ways to use Go generics and we will undoubtedly make plenty of mistakes along the way. Considering that, Rob Pike's suggestion to keep generics out of most of the standard library for the Go 1.18 release seems very wise.

Finally, here's a plug for the gmath library, my attempt to write generic versions of commonly used functions from the Go math package. Check it out if you need math functions for numeric types other than float64!

💖 💪 🙅 🚩
matthewdale
Matt Dale

Posted on March 1, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related