Go 博客
生成代码
通用计算的一个特性——图灵完备性——是计算机程序可以编写计算机程序。这是一个强大的想法,尽管它经常发生,但它的重要性并没有得到充分的认识。例如,它是编译器定义的一个重要部分。go
test
命令的工作方式也是如此:它扫描要测试的包,编写一个包含为该包定制的测试框架的 Go 程序,然后编译并运行它。现代计算机速度如此之快,这种听起来很昂贵的序列可以在一秒钟内完成。
程序生成程序的例子还有很多。例如,Yacc 读取语法描述并编写一个解析该语法的程序。协议缓冲区“编译器”读取接口描述并发出结构定义、方法和其他支持代码。各种各样的配置工具也是这样工作的,它们检查元数据或环境并发出针对本地状态定制的脚手架。
因此,程序生成程序是软件工程中的重要元素,但像 Yacc 这样生成源代码的程序需要集成到构建过程中,以便它们的输出可以被编译。当使用 Make 等外部构建工具时,这通常很容易做到。但在 Go 中,它的 go 工具从 Go 源代码获取所有必要的构建信息,这就存在一个问题。仅凭 go 工具,根本没有办法运行 Yacc。
直到现在。
最新的 Go 版本 1.4 包含了一个新的命令,使得运行此类工具更加容易。它叫做 go
generate
,它通过扫描 Go 源代码中的特殊注释来识别要运行的通用命令。重要的是要理解 go
generate
不是 go
build
的一部分。它不包含任何依赖分析,并且必须在运行 go
build
之前显式运行。它intended to be used by the author of the Go package, not its clients.(它intended to be used by the author of the Go package, not its clients.)。
go
generate
命令易于使用。作为热身,下面是如何使用它来生成 Yacc 语法。
首先,安装 Go 的 Yacc 工具
go get golang.org/x/tools/cmd/goyacc
假设你有一个名为 gopher.y
的 Yacc 输入文件,它定义了你新语言的语法。要生成实现该语法的 Go 源文件,你通常会像这样调用命令:
goyacc -o gopher.go -p parser gopher.y
-o
选项指定输出文件,而 -p
指定包名。
要让 go
generate
驱动整个过程,只需在同一个目录中的任何一个常规(非生成的).go
文件中,在文件中的任何位置添加此注释:
//go:generate goyacc -o gopher.go -p parser gopher.y
这段文本只是上面命令的前缀,后面跟着一个 go
generate
识别的特殊注释。注释必须从行的开头开始,并且 //
和 go:generate
之间不能有空格。在该标记之后,行的其余部分指定了一个 go
generate
要运行的命令。
现在运行它。切换到源目录并运行 go
generate
,然后是 go
build
等等。
$ 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
工具中——无需额外安装——并且很好地融入了 Go 生态系统。只是要记住,它适用于包作者,而不是客户端,即使只是因为它调用的程序可能在目标机器上不可用。此外,如果包含的包 intended to be imported by go
get
,一旦文件生成(并测试!)完成后,它必须被提交到源代码存储库才能供客户端使用。
既然有了它,我们就用它来做一些新的事情。作为 go
generate
如何提供帮助的一个非常不同的例子,在 golang.org/x/tools
存储库中有一个名为 stringer
的新程序。它可以自动为一组整数常量编写字符串方法。它不是发布分发版的一部分,但很容易安装。
$ 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)
}
当然,还有其他方法可以编写这个函数。我们可以使用一个由 Pill 索引的字符串切片,或者一个映射,或者其他技术。无论我们做什么,如果我们改变了药丸的集合,我们就需要维护它,并且我们需要确保它是正确的。(扑热息痛的两个名称使这比其他情况更棘手。)此外,选择哪种方法取决于类型和值:有符号或无符号,密集或稀疏,零基或非零基,等等。
stringer
程序负责所有这些细节。虽然它可以独立运行,但它intended to be driven by 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
来更新 String
方法。当然,如果我们同一个包中有多个类型以这种方式设置,那么单个命令将通过一个命令更新它们所有的 String
方法。
毫无疑问,生成的代码方法很难看。不过没关系,因为人类不需要处理它;机器生成的代码通常很难看。它正在努力变得高效。所有名称都被压缩成一个字符串,这可以节省内存(即使有无数个名称,也只有一个字符串头)。然后一个名为 _Pill_index
的数组通过一种简单高效的技术将值映射到名称。请注意,_Pill_index
是一个数组(而不是切片;又少了一个头部)类型为 uint8
,这是跨越值空间所需的最小整数。如果值更多,或者有负数,_Pill_index
的生成类型可能会更改为 uint16
或 int8
:任何最有效的方法。
stringer
打印的方法所使用的方法会根据常数集的大小而变化。例如,如果常量是稀疏的,它可能会使用一个映射。下面是一个基于表示二的幂的常量集的小示例:
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
包中生成 Unicode 表,在 encoding/gob
中为数组的编码和解码创建高效的方法,在 time
包中生成时区数据等等。
请创造性地使用 go
generate
。它旨在鼓励实验。
即使你不使用它,也要使用新的 stringer
工具为你的整数常量编写 String
方法。让机器来完成这项工作。
下一篇文章:Gopher Gala 是全球首个 Go 编程马拉松
上一篇文章:Go 1.4 发布
博客索引