
Go 1.18 is scheduled for release sometime soon (originally Feb 2022) and will be the first version of Go to supportgeneric 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 goodarticles,tutorials, andtalks 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 latestgotip
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 theC++ 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 simpleMin
function that accepts two numeric values and returns the minimum value of the two. OurMin
function can accept two parameters, both either anint64
orfloat64
, and returns a value the same type as the inputs.
funcMin[Tint64|float64](x,yT)T{ifx<y{returnx}returny}
To call our genericMin
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.
varxint64=5varyint64=12Min(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 afloat32
orfloat64
:
funcInf[Tfloat32|float64](t)T{returnT(math.Inf(1))}
Now let's try to call theInf
function to assign a+Inf
value to afloat64
variable:
varxfloat64x=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:
varxfloat64x=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 variablehere, 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 functionDo
that takes anint
value and performs some action.
funcDo(xint){// Perform some action with int x.}
Now let's say we wantDo
to support either anint
or abool
, and perform a different action for with each type. Before generics, we might have just added new functionsDoInt
andDoBool
, and kept theDo
function to maintain API backward compatibility.
funcDoInt(xint){// Perform some action with int x.}funcDoBool(xbool){// Perform some action with bool x.}funcDo(xint){DoInt(x)}
With generics, we can add a type constraint to ourDo
function to accept either anint
or abool
and use a type switch to do the appropriate action for each type.
funcDo[Tint|bool](xT){switchany(x).(type){caseint:// Perform some action with int x.casebool:// Perform some action with bool x.}}
Note:any
is a new shorthand forinterface{}
.
We've reduced our API surface area by only using a singleDo
function, and maintained API backward compatibility in most cases (calls toDo
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 opendiscussion 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 discussionhere).
As a result, you may end up with quite large tests with lots of subtests and lots of test cases, like the oneshere. 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'ssuggestion 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 thegmath
library, my attempt to write generic versions of commonly used functions from the Gomath
package. Check it out if you needmath
functions for numeric types other thanfloat64
!
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse