Last year Ian Lance Taylor and Robert Griesemer released a new draft of Go generics (The Next Step for Generics), and the response from Gopher at home and abroad was overwhelming, with everyone interpreting the draft and this article, and feeling that this version of Go generic design is basically close to Go’s generic goals, and in short, much better than the previous one.

Ian also provided an online compilation tool, go2go, to get a taste of Go generic programming.

What if you compile locally?

Actually Go source code is synced to github, so you just need to download the appropriate branch, compile it yourself, and get this go2go tool. This article guides you if you download, compile, and use this tool, and you can also learn how Go generic code is converted to Go1 code and then run.

Of course, as always, the current design and tools are designed for the draft version, and will change when the official version is released.

Installation

First download the Go code, the branch is dev.go2go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# clone go Source Code
$ cd $HOME
$ mkdir go2go
$ cd go2go
$ git clone -b dev.go2go  git@github.com:golang/go.git goroot
$ cd goroot
# Compiling Go
$ cd src
$ ./make.bash
# You can write the following environment variable settings to a bash file for later use
# It sets the Go2 path and root, and adds them to the path environment variable
$ export GO2GO_DEST=$HOME/go2go/goroot
$ export PATH="$GO2GO_DEST/bin:$PATH"
$ export GOROOT="$GO2GO_DEST"
$ export GO2PATH="$GO2GO_DEST/src/cmd/go2go/testdata/go2path"
# View go version
$ go version
$ go version devel +5e754162cd Thu Jun 18 05:58:40 2020 +0000 darwin/amd64

By following the steps above, you will be able to compile the latest Go tools that support Go generics.

Writing Go generic code

Next let’s write a Go generic application:

go

In this example, we define a NumberString interface, which is an extension of the interface, and you can have only numbers or strings implement this interface with the following declaration:

1
2
3
4
5
type int,int8,int16,int32,int64,
	 uint,uint16,uint32,uint64,
	 float32,float64,
	 complex64,complex128,
	 byte,uintptr,string

The main purpose is still to constrain the types in the generic. Because we want to use the + symbol in the body of functions that use generic arguments, only numbers and strings support this operator, so in order for the function to compile properly, you need to constrain the type arguments. The Go compiler finds objects that are NumberString objects when it compiles them, so they can be summed using the + operator.

In this case, the NumberString interface cannot be implemented by other types, for example, the following code will compile with an error:

1
2
var c3 NumberString = time.Now() // Error, time.Time cannot implement NumberString
fmt.Println(c3)

Also note that the go2 code files are currently suffixed with .go2 to distinguish them from the Go1 code.

You can now compile and run the above code:

1
2
3
$ go tool go2go run monoid.go2
3
hello world!

How is the Go2 code compiled?

go2go converts Go2 code to go1 code for running, which means that it provides generic support through compile-time conversions. So the generic design of Go is relatively simple, and Go2 also provides backward compatibility.

You can see what magic go2go does by converting Go2 code to Go1 code with the following command.

1
$ go tool go2go translate monoid.go2

The converted Go1 code is as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Code generated by go2go; DO NOT EDIT.
//line monoid.go2:1
package main
//line monoid.go2:1
import "fmt"
//line monoid.go2:23
func main() {
	c := instantiate୦୦Concat୦int{}
	fmt.Println(c.Combine(1, 2))
	c2 := instantiate୦୦Concat୦string{}
	fmt.Println(c2.Combine("hello ", "world!"))
}
//line monoid.go2:28
type instantiate୦୦Concat୦int struct{}
//line monoid.go2:18
func (c instantiate୦୦Concat୦int,) Combine(x int, y int) int {
	return x + y
}
//line monoid.go2:20
type instantiate୦୦Concat୦string struct{}
//line monoid.go2:18
func (c instantiate୦୦Concat୦string,) Combine(x string, y string) string {
	return x + y
}
//line monoid.go2:20
type Importable୦ int
//line monoid.go2:20
var _ = fmt.Errorf

As you can see, for the generic code in the code, because the type parameter needs to be instantiated when instantiating, go2go specializes the generic code, generating a special type for each type.

So in our example above, the types of c1 and c2 are different, and their types are instantiate୦୦Concat୦int and instantiate୦ ୦Concat୦string. Use instantiate as prefix and type as suffix int,with ୦୦ and ୦୦୦ as hyphen.

If the type parameters are the same, the same specialised type is used, e.g. c1 and c3 in the following example, both use the same specialised type instantiate୦୦Concat୦int.

1
2
3
4
5
6
7
8
func main() {
	c  := Concat(int){}
	fmt.Println(c.Combine(1,2))
	c2 := Concat(string){}
	fmt.Println(c2.Combine("hello ","world!"))
	c3 := Concat(int){}
	fmt.Println(c3.Combine(10,20))
}

The real code logic of go2go is in go/go2go, which provides the logic for code parsing and conversion, and you can savor the implementation by Ian Lance Taylor and Robert Griesemer. I believe there will be an in-depth Gopher analysis article coming soon.

Then the go2go tool entry code is at cmd/go2go.

Mess up a bit

Since the go2go tool translates Go2 code into Go1 code and specializes the generic types, what happens if we declare a type with the same name as the specialized type? For example, the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main 
import "fmt"
type NumberString interface {
	type int,int8,int16,int32,int64,
		 uint,uint16,uint32,uint64,
		 float32,float64,
		 complex64,complex128,
		 byte,uintptr,string
}
type Monoid(type A) interface{
	Combine(x A, y A) A
}
type Concat(type A NumberString) struct{}
func(c Concat(A)) Combine(x A, y A) A {
	return x + y 
}
// A type that is renamed with the specialised type
type instantiate୦୦Concat୦int struct{}
func main() {
	c  := Concat(int){}
	fmt.Println(c.Combine(1,2))
	c2 := Concat(string){}
	fmt.Println(c2.Combine("hello ","world!"))
	c3 := Concat(int){}
	fmt.Println(c3.Combine(10,20))
}

We declare the instantiate୦୦Concat୦int type in the code and then compile and run it:

1
2
3
4
5
6
7
$go tool go2go run monoid.go2
# command-line-arguments
/var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go2go-run795741129/monoid.go2:26: c.Combine undefined (type instantiate୦୦Concat୦int has no field or method Combine)
/var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go2go-run795741129/monoid.go2:31: c3.Combine undefined (type instantiate୦୦Concat୦int has no field or method Combine)
/var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go2go-run795741129/monoid.go2:32: instantiate୦୦Concat୦int redeclared in this block
	previous declaration at /var/folders/gq/jd9v5dd95p570hkztblb8ht40000gn/T/go2go-run795741129/monoid.go2:22
/Users/xxxxxx/go2go/goroot/bin/go [run monoid.go] failed: exit status 2

The result is that the compilation fails because the go2go conversion program also generates a renamed instantiate୦୦Concat୦int type.

Of course go2go is still a conceptual tool and will certainly do optimization and type case handling in the future, the related technology is called Name mangling, which is something the compiler will do.


Reference https://colobu.com/2020/06/18/run-local-go-generic-files/