本地运行Go泛型代码
昨天 Ian Lance Taylor 和 Robert Griesemer 发布了Go泛型的新的草案( The Next Step for Generics ), 国内外的Gopher反响非常的热烈,大家纷纷对草案和这个文章进行了解读,并且感觉这一版的Go泛型设计基本接近于Go的泛型目标,总之比前一个方案好太多了。
同时Ian也提供了一个在线编译的工具 go2go ,可以对Go泛型编程进行尝鲜。
如果在本地编译呢?
事实上Go的源代码会同步到github中,所以你只需要下载相应的分支,自己进行编译,就可以得到这个go2go工具。本文指导你如果下载、编译、使用这个工具,而且你还可以学习到Go泛型代码是如何转换成Go1代码,然后运行的。
当然,还是那句话,当前的设计和工具都是为草案版设计的,正式版的时候会有所变化。
安装
首先下载Go代码,分支是 dev.go2go :
# clone go源代码 $ cd $HOME $ mkdir go2go $ cd go2go $ git clone -b dev.go2go git@github.com:golang/go.git goroot $ cd goroot # 编译Go $ cd src $ ./make.bash # 你可以把下面的环境变量的设置写到一个bash文件中,方便以后使用 # 它设置了Go2对应的path和root,并加入到path环境变量中 $ 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" # 查看go版本 $ go version $ go version devel +5e754162cd Thu Jun 18 05:58:40 2020 +0000 darwin/amd64
通过上面的步骤,你就可以编译好最新的支持Go泛型的Go工具。
编写Go泛型代码
下一步让我们编写一个Go泛型的应用:

在这个例子中,我们定义了一个 NumberString 接口,这是接口的一个扩展功能,你可以通过以下的声明,只让数字或者字符串实现这个接口:
type int,int8,int16,int32,int64,
uint,uint16,uint32,uint64,
float32,float64,
complex64,complex128,
byte,uintptr,string
主要的用途还是为了对泛型中的类型进行约束。因为我们要在使用泛型参数的函数体中使用 + 符号,只有数字和字符串支持这个操作符,所以为了让函数能正常的编译,你需要对类型参数进行约束。Go编译器在编译的时候,发现对象是 NumberString 的对象,所以可以使用 + 操作符进行相加。
在这种情况下, NumberString 接口不能被其它类型所实现,比如下面的代码就会编译出错:
var c3 NumberString = time.Now() // 出错, time.Time不能实现NumberString fmt.Println(c3)
另外需要注意的是go2的代码文件当前以 .go2 为后缀,以便和Go1的代码相区分。
下面就可以编译运行上面的代码了:
$ go tool go2go run monoid.go2 3 hello world!
Go2代码是如何编译的?
go2go把Go2代码转换成go1的代码进行运行的,也就是说通过编译期的转换,提供泛型的支持。 所以Go的泛型设计相对简单,并且Go2也提供了向下兼容。
你可以通过下面的命令将Go2代码转换成Go1的代码,可以查看go2go做了什么魔法:
$ go tool go2go translate monoid.go2
转换成的Go1代码如下:
// 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
可以看到,对于代码中的泛型代码,因为在实例化的时候需要实例化类型参数,所以go2go将泛型代码进行了 特化 ,针对每个类型生成了一个特化的类型。
所以在我们上面的例子中, c1 和 c2 的类型是不同的,它们的类型分别是 instantiate୦୦Concat୦int 和 instantiate୦୦Concat୦string 。采用 instantiate 做前缀,类型做后缀 int ,以 ୦୦ 和 ୦ 做连字符。
如果类型参数相同,会采用同一个特化的类型,比如下面例子中的 c1 和 c3 ,都使用同一个特化的类型 instantiate୦୦Concat୦int :
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))
}
go2go真正的代码逻辑在 go/go2go ,它提供了代码解析和转换的逻辑,你可以仔细品一品Ian Lance Taylor 和 Robert Griesemer的实现。相信不久就会有Gopher深度解析的文章问世。
然后go2go的工具入口代码在 cmd/go2go 。