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
}
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
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))
}
Now let's try to call the Inf
function to assign a +Inf
value to a float64
variable:
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:
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, 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.
}
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)
}
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.
}
}
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
!
Top comments (0)