Notes on the Go generics proposal

Update: The proposal draft has been revisited to use brackets instead of parenthesis. This article will be updated with the new syntax soon.

Ian Lance Taylor and Robert Griesemer have been working on a generics proposal for Go for a while. Unlike other proposals, a highly significant language change as such generics will require experimentation and comprehensive feedback before it can be finalized and submitted as a formal language change proposal.

As an initial step, they have been working on an transitioning tool so the Go users can put their ideas into test and have working understanding of the proposal. go2go is a tool available if you install Go from the source. Then you can build, run and test .go2 files with generics.

$ go tool go2go
go2go <command> [arguments]

The commands are:

	build      translate and build packages
	run        translate and run list of files
	test       translate and test packages
	translate  translate .go2 files into .go files

Alternatively, they are providing the same capability from the go2go playground, so the Go developers can easily test their ideas without having to install Go from source. I highly recommend you to take a look at the playground to begin.

When I was going through a draft of the proposal to put some of my ideas into test, there were many gotchas moments I had. In this article, I’ll share some of them to help you with your own experimentation efforts. This article is a living document and might be updated as I discover more.

Please read the proposal first, the current version of the proposal contains most of the answers. If you have similar questions, report them to provide feedback. Readability of this feature will be significantly important and your feedback is very critical given generics make languages more complicated to read.

What can be generic?

Functions can be generic, similar to the Print function below. At the call site, Print can be called against any type that will determine what T is.

func Print(type T)(s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

Print(int)([]int{1, 2, 3}) // will print 123

Structs can also rely on generic types. A list that uses a generic item type can be defined as:

type List(type T) struct {
	list []T
}

When constructing a new List, users provide the item type. This is how you can use the same List implementation against different item types. In the example below we are creating a list of integers:

list := List(int){}

Interfaces can represent cases where methods need to use generic types. For example, a database iterator interface is provided below. Implementors of this interface will provide generic mechanisms that will iterate over a user-specified type:

type Iterator(type T) interface {
	Next() (T, error)
}

Currently, methods cannot have their own generic signatures. The Do function below is accepting a generic type, C as its arguments. The code will not compile. If you have cases where being able to provide generic methods

type List(type T) struct {
	list []T
}

func (l *List(T)) Do(type C)(c []C) {}
prog.go2:20:26: methods cannot have type parameters

Can receivers accept generic types?

Yes. If you have a generic concrete type, its recievers can accept generic types and refer to the generic type in their arguments. Below, the generic List can use any user provided type as its item type. Users will be able to call Add with the objects of type T – a type they provided to construct List(T).

type List(type T) struct {
	list []T
}

func (l *List(T)) Add(item T) {}

How to constraint a generic type?

Generic types can be of a particular interface or the empty interface.

func Print(type T)(s []T) {} // accepts every type
func Print(type T io.Writer)(s []T) {} // accepts types if T implements io.Writer

How to allocate a generic slice?

Defining, initializing or allocating generic slices are not different than any regular slice. All the operations on T below are valid operations:

func Do(type T)(s []T) {
	var t []T 
	t = make([]T, 10)
	t = append(t, s...)
}

Can you compose generic interfaces?

Yes. Interface composition is working as expected. If you want to provide a specialized version of the Iterator(type T), you can defer to composition.

type Iterator(type T) interface {
	Next() (T, error)
}

type IteratorCloser(type T) interface {
	(Iterator(T))
	Close() error
}

How to call a generic functions generically?

Calling generic functions from a generic function is pretty straightforward. You can pass the generic type identifier, T, to another function.

func Print(type T)(s []T) {
	for _, v := range s {
		PrintOne(T)(v)
	}
}

func PrintOne(type T)(s T) {
	fmt.Print(s)
}

At first, this confused me because argument identifiers in Go are lower case. So, I didn’t see the T is another identifier until I’m told so. The upside of having T in upper case is it allows me to differentiate the generic type list from the argument list. I hope the upper case becomes the convention.

Are type assertions possible?

Yes, but this confused me initially. If you tried to type cast directly on the generic variable such as below, you will get an error saying r is not an interface type, even though r implements io.Reader.

func Do(type T io.Reader)(r T) {
	switch r.(type) {
	case io.ReadCloser:
		fmt.Println("ReadCloser")
	}
}
prog.go2:19:9: r (variable of type T) is not an interface type

But when you explicitly cast r to an interface before casting:

func Do(type T io.Reader)(r T) {
	switch (interface{})(r).(type) {
	case io.ReadCloser:
		fmt.Println("ReadCloser")
	}
}