Generics in Go – How They Work and How to Play With Them

Generics in Go have come much closer to becoming a reality. Here's what the latest design looks like and how you can try out generics yourself.

Generics in Go – How They Work and How to Play With Them

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:

type Stack []interface{}

func (s Stack) Peek() interface{} {
	return s[len(s)-1]
}

func (s *Stack) Pop() {
	*s = (*s)[:len(*s)-1]
}

func (s *Stack) Push(value interface{}) {
	*s = append(*s, value)
}
Run this in the Playground.

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:

type Stack(type T) []T

func (s Stack(T)) Peek() T {
	return s[len(s)-1]
}

func (s *Stack(T)) Pop() {
	*s = (*s)[:len(*s)-1]
}

func (s *Stack(T)) Push(value T) {
	*s = append(*s, value)
}
Run this in the WebAssembly Playground.

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 Pushing 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:

contract stringer(T) {
	T String() string
}

type Stack(type T stringer) []T

// Now we can use the String method of T:
func (s Stack(T)) String() string {
	ret := ""
	for _, v := range s {
		if ret != "" {
			ret += ", "
		}
		ret += v.String()
	}
	return ret
}
Run this in the WebAssembly Playground.

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:

https://go.googlesource.com/proposal/+/4a54a00950b56dd0096482d0edae46969d7432a6/design/go2draft-contracts.md

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:

git fetch "https://go.googlesource.com/go" refs/changes/17/187317/14 && git checkout FETCH_HEAD
This will checkout patchset 14 of the CL.

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


Share Tweet Send
0 Comments
Loading...