Go is a bit infamous for not supporting generics, but lately generics have come much closer to becoming a reality. There's a draft design that seems to be relatively stable and is gaining traction in the form of a prototype source-to-source translator implemented by the Go team. Here's what the latest design looks like and how you can try out generics yourself.
Examples
LIFO Stack
Let's say you want to create a last-in, first-out stack. Without generics, you would probably implement it like this:
There's a problem here though: Whenever you Peek
at an item, you have to use a type assertion to convert it from interface{}
to something useful. If your stack is a stack of *MyObject
that means a lot of s.Peek().(*MyObject)
. Not only is that an eye-sore, but it invites room for error. What if you forget the asterisk? Or what if you push the wrong type? s.Push(MyObject{})
would happily compile, and you might not realize your mistake until it's impacting your service.
In general using interface{}
is relatively dangerous. It's always safer to use more restricted types so issues can be discovered at compile-time instead of run-time.
Generics solves this problem by allowing types to have type parameters:
This adds a type parameter to Stack
, eliminating the need for interface{}
altogether. Now, when you Peek()
, the value returned is already the original type and there's no chance of Push
ing the wrong type of value. This implementation is all-around much safer and easier to use.
Additionally, generic code is generally easier for the compiler to optimize, resulting in better performance (at the expense of binary size). If we benchmark the above non-generic and generic code, we can see the difference:
type MyObject struct {
X int
}
var sink MyObject
func BenchmarkGo1(b *testing.B) {
for i := 0; i < b.N; i++ {
var s Stack
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink = s.Peek().(MyObject)
}
}
func BenchmarkGo2(b *testing.B) {
for i := 0; i < b.N; i++ {
var s Stack(MyObject)
s.Push(MyObject{})
s.Push(MyObject{})
s.Pop()
sink = s.Peek()
}
}
BenchmarkGo1
BenchmarkGo1-16 12837528 87.0 ns/op 48 B/op 2 allocs/op
BenchmarkGo2
BenchmarkGo2-16 28406479 41.9 ns/op 24 B/op 2 allocs/op
In this case we allocate less memory and run at twice the speed using generics.
Contracts
The stack example above works for any type. However, there are many cases where you need to write code that works only on types with certain traits. For example, you might want your stack to require that types implement the String()
function. This is where "contracts" come in:
More Examples
The above examples only cover the basics. You can also add type parameters to functions and add specific types to contracts.
For more examples, there are two places you can go to:
The Draft Design
The draft design contains a more detailed description as well as several more examples:
The Prototype CL
The prototype CL has several examples as well. Look for the files that end in ".go2":
https://go-review.googlesource.com/c/go/+/187317
How You Can Try Generics Out Today
Using the WebAssembly Playground
By far the fastest and easiest way to try out generics is through the WebAssembly playground. Behind the scenes, it uses a WASM build of the prototype source-to-source translator to run Go code directly in your browser. This does have some restrictions though (see github.com/ccbrown/wasm-go-playground).
Compiling The CL
The CL referenced above contains an implementation of a source-to-source translator that can be used to compile generic code to code that can be compiled by the current releases of Go. It refers to generic code ("polymorphic" code) as Go 2 code and non-polymorphic code as Go 1 code, but depending on the details of the implementation, generics could become part of a Go 1 release rather than a Go 2 release.
The CL expands the existing go/* packages to include support for generics and adds a new go/go2go package, which can be used to rewrite generic Go 2 code to Go 1 code.
It also adds a "go2go" command that can be used to translate code from your CLI.
You can compile the CL by following Go's Installing Go from Source instructions. When you get to the optional "Switch to the master branch" step, checkout the CL instead:
Note that this will checkout patchset 14, which is the latest at the time of writing. Go to the CL and find the "Download" button to get the checkout command for the latest patchset.
After compiling the CL, you can use the go/* packages to write custom tooling for working with generics, or you can just use the go2go
command-line tool:
go tool go2go translate mygenericcode.go2