[Go] 译文 Generating code - The Go Blog
Rob Pike 22 December 2014
通用计算有一个特性名为“图灵完整性”,他意味着计算机程序可以由其他计算机程序编写完成。这是一个很厉害想法,但却没有得到应得的欣赏,计算机程序可以由其他计算机程序编写这种事情经常在身边发生。这其实是编译器的重要组成部分,举个例子看下 go test
命令是如何工作的:它首先扫描需要被测试的 package,然后生成为该 package 定制的测试代码,并编译执行。现代计算机已经很快了,所以上面听起来复杂的步骤执行起来只需要不到一秒。
计算机程序编写计算机程序其实是有很多案例的。 譬如:Yacc 它读取输入的语法描述,然后生成解析语法的程序。 protocol buffer 的编译器也是类似,它读取接口描述并生成相应的结构定义以及函数等相关代码。又或者很多配置工具是这样工作的:为本地内容检查 metadata 、 environment 及相关的脚手架。
可以看出程序编写程序是软件工程领域里面非常重要的一部分。
一般我们会将 Yacc 集成到构建工具中,譬如 Make,这样我们便可以控制相关过程细节。如果想在 go tool 中使用 Yacc 是非常困难的,因为 go 的 go tool 相关工具只能从 go 源代码获取所需信息,并没有能够动态运行 Yacc 的相关机制功能。
在最新的 release 版本 1.4
中,这个问题得到解决,该版本新增了可以方便使用类似 Yacc 等相关工具的一个命令。这个命令便是 go generate
,它的工作机制为:扫描包含通过注释指定的 generate 代码并执行。因为不包含相关依赖的分析过程,所以这个命令并不是 go build
的一部分,一般在 go build
执行前执行。这个功能是为 go 的开发者设计的,而不是一个客户端功能。
go generate
命令非常容易使用,做里举一个如何使用 Yacc 生成代码的例子热热身。
首先,安装 Go’Yacc tool:
go get golang.org/x/tools/cmd/goyacc
假设你有一个描述了新语言的语法文件叫做 gopher.y
。一般情况下如果需要生成实现该语法的 go 代码,你需要执行该语句:
goyacc -o gopher.go -p parser gopher.y
-o 参数表述输出文件名,-p 参数表示 package 名。
我们可以通过 go generate
去驱动这个进程,在当前目录下的任意非生成文件的任意位置加上下面的注释:
//go:generate goyacc -o gopher.go -p parser gopher.y
上面这个包含特定前缀的注释便是 go generate
的识别内容。这个注释必须出现在一行的开始处,并且 //
之间不能有空格,//
与 go:generate
之间也不能有空格。除了这些,剩下的便是你需要 go generate
替你执行的命令部分。
接着我们回到代码目录执行命令:
$ cd $GOPATH/myrepo/gopher
$ go generate
$ go build
$ go test
这便完成了一切,我们假设这个过程没有产生异常,go generate
命令会执行 yacc 并生成 gopher.go 文件,然后在持有 go 文件的当前目录我们可以执行编译、测试等等。每次只要更新了 gopher.y 文件,只需要重复上述 go generate
去生成新的解析代码即可。
如果想了解更多关于 go generate
如何工作的细节,包括选项,环境变量等等,可以查看 设计文档
。
go generate
能够做一些其他 make 工具无法完成的事情,而且还是 go tool 内置的,所以非常适合 go 的生态系统。为什么说它是为开发者设计的而不是一个客户端使用的工具呢,因为它所执行的程序可能在目标机器上并不可用。而且如果它需要通过 go get
获取一些 package 的话,生成的内容完成测试后合入项目版本中,客户端才能用。
接着我们尝试些其他事情,stringer
是 golang.org/x/tools
项目下的新程序,它和前面 go generate
的案例不同。它能够自动生成关于常量集合的 string 方法,它并没有随着 release 版本发布,但是安装很容易:
$ go get golang.org/x/tools/cmd/stringer
这里有个 stringer 的例子:
package painkiller
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
一般为了方便调试,我们会为这些常量添加一些输出的签名。
func (p Pill) String() string
譬如实现如下:
func (p Pill) String() string {
switch p {
case Placebo:
return "Placebo"
case Aspirin:
return "Aspirin"
case Ibuprofen:
return "Ibuprofen"
case Paracetamol: // == Acetaminophen
return "Paracetamol"
}
return fmt.Sprintf("Pill(%d)", p)
}
当然,还有其他的实现方式,我们可以使用 string slice 来持有 Pill 或则 map 亦或其他的技术,无论怎么做,只要这些内容发生变动,你就需要主动去保证其正确性,何况还需要考虑数据的类型和值的相关问题。
stringer 程序解决了这些所有的问题,尽管其可以独立运行,但是其更倾向于通过 go generate
去驱动。如何使用呢,只需要在具体类型附近添加下面的注释便可:
//go:generate stringer -type=Pill
该注释的意思是使用 go generate
运行 stringer 生成类型 Pill 的 String 相关函数。输出的文件默认为 pill_string.go ,当然可以通过 -output 参数更改默认值。
$ go generate
$ cat pill_string.go
// Code generated by stringer -type Pill pill.go; DO NOT EDIT.
package painkiller
import "fmt"
const _Pill_name = "PlaceboAspirinIbuprofenParacetamol"
var _Pill_index = [...]uint8{0, 7, 14, 23, 34}
func (i Pill) String() string {
if i < 0 || i+1 >= Pill(len(_Pill_index)) {
return fmt.Sprintf("Pill(%d)", i)
}
return _Pill_name[_Pill_index[i]:_Pill_index[i+1]]
}
每当我们更新了 Pill 的定义或则常量的定义,我们只需要执行这个命令即可:
$ go generate
除此之外,如果你在当前 package 里的多个定义使用 stringer,一样是上面一个命令完成所有的定义的 String 方法更新。
虽然生成的代码有点丑,但是好在你不需要去编辑它,一般而言,机器生成的代码都是挺丑的,但是能提高工作的效率。通过将所有的字符串合在一起,这样可以有效的节省内存。_Pill_index 是一个数组,用于映射具体的 name,这是一种非常简单高效的技术。_Pill_index 是一个 uint8 类型的数组,而不是 slice,uint8 为足够这里使用的最小整数,如果有更多的值,可能会提升到 uint16 或则 int8,具体视那种工作的更好。
如果你的常量集是稀疏类型的,则生成的代码可能会使用 map:
const _Power_name = "p0p1p2p3p4p5..."
var _Power_map = map[Power]string{
1: _Power_name[0:2],
2: _Power_name[2:4],
4: _Power_name[4:6],
8: _Power_name[6:8],
16: _Power_name[8:10],
32: _Power_name[10:12],
...,
}
func (i Power) String() string {
if str, ok := _Power_map[i]; ok {
return str
}
return fmt.Sprintf("Power(%d)", i)
}
总得来说,自动生成函数能够让我们更专注于适合人类去做的事情。
Go 内部也有大量通过 go generate
生成的代码,譬如 unicode package 中的 Unicide 表,encoding/gob 里高效编解码函数, time package 里的时间零值,等等。
通过 go generate
开始你的创造之旅吧,再不济也尝试使用 stringer 让机器代替你去生成那些整形常量的 String 函数吧。