Early Perspectives on Go Generics
Tags: go, generics
Published: 2022-03-01 07:26:31

[Go 1.18](https://tip.golang.org/doc/go1.18) is scheduled for release sometime soon (originally Feb 2022) and will be the first version of Go to support [generic programming](https://en.wikipedia.org/wiki/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](https://www.youtube.com/watch?v=WzgLqE-3IhY&t=39s)

There are already lots of good [articles](https://go.dev/blog/why-generics), [tutorials](https://go.dev/doc/tutorial/generics), and [talks](https://www.youtube.com/watch?v=Pa_e9EeCdy8) 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`](https://pkg.go.dev/golang.org/dl/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](https://en.wikipedia.org/wiki/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](http://stepanovpapers.com/history%20of%20STL.pdf)

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.

```go
func Min[T int64 | float64](x, y T) T {
	if x < y {
		return x
	}
	return y
}
```

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.


```go
var x int64 = 5
var y int64 = 12
Min(x, y) // returns 5
```

For a more in-depth look at generics in Go, check out the [(beta) Go generics tutorial](https://go.dev/doc/tutorial/generics).

## 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`:
```go
func Inf[T float32 | float64](t) T {
	return T(math.Inf(1))
}
```
Now let's try to call the `Inf` function to assign a `+Inf` value to a `float64` variable:

```go
var x float64
x = Inf()
```

Oops, we got a compile error!

```
./main.go:20:15: cannot infer T (./main.go:10:10)
```

Instead, we have to specify the function type explicitly:
```go
var x float64
x = Inf[float64]()
```

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](https://github.com/golang/go/issues/50285), 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.

```go
func Do(x int) {
	// Perform some action with int x.
}
```

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.

```go
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)
}
```

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.

```go
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.
	}
}
```
_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](https://github.com/golang/go/issues/45380) 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](https://groups.google.com/g/golang-nuts/c/ulntjLeGYn4)).

As a result, you may end up with quite large tests with lots of subtests and lots of test cases, like the ones [here](https://github.com/matthewdale/gmath/blob/main/gmath_test.go). 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](https://github.com/golang/go/issues/48918) 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`](https://github.com/matthewdale/gmath) library, my attempt to write generic versions of commonly used functions from the Go [`math`](https://pkg.go.dev/math) package. Check it out if you need `math` functions for numeric types other than `float64`!