本地运行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将泛型代码进行了 特化 ,针对每个类型生成了一个特化的类型。

所以在我们上面的例子中, c1c2 的类型是不同的,它们的类型分别是 instantiate୦୦Concat୦intinstantiate୦୦Concat୦string 。采用 instantiate 做前缀,类型做后缀 int ,以 ୦୦ 做连字符。

如果类型参数相同,会采用同一个特化的类型,比如下面例子中的 c1c3 ,都使用同一个特化的类型 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