Go 博客
生成代码
通用计算的一个特性——图灵完备性——就是计算机程序可以编写计算机程序。这是一个强大的想法,尽管它经常发生,但并没有得到应有的重视。例如,它是编译器定义的重要组成部分。go
test
命令也是这样工作的:它扫描要测试的包,写出一个包含为该包定制的测试套件的 Go 程序,然后编译并运行它。现代计算机非常快,这段听起来很费时的序列可以在几分之一秒内完成。
还有很多其他程序编写程序的例子。例如,Yacc 读取语法描述并写出一个程序来解析该语法。协议缓冲区“编译器”读取接口描述并输出结构定义、方法和其他支持代码。各种配置工具也都是这样工作的,检查元数据或环境并输出根据本地状态定制的脚手架。
因此,编写程序的程序是软件工程中的重要元素,但像 Yacc 这样生成源代码的程序需要集成到构建过程中,以便其输出可以被编译。当使用像 Make 这样的外部构建工具时,这通常很容易做到。但在 Go 中,其 go 工具从 Go 源代码获取所有必要的构建信息,这里有一个问题。仅靠 go 工具根本没有机制来运行 Yacc。
也就是说,直到现在。
最新的 Go 发布版本 1.4 包含一个新的命令,可以更轻松地运行此类工具。它叫做 go
generate
,它的工作原理是扫描 Go 源代码中用于标识要运行的通用命令的特殊注释。重要的是要理解 go
generate
不是 go
build
的一部分。它不包含任何依赖分析,并且必须在运行 go
build
之前显式运行。它旨在供 Go 包的作者使用,而不是其客户端。
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 生态系统。请记住,它是用于包作者,而不是客户端,仅仅是因为它调用的程序可能在目标机器上不可用。此外,如果包含的包旨在通过 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
)
为了调试,我们希望这些常量能够自如地美观打印,这意味着我们需要一个具有如下签名的“String”方法:
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 索引的字符串切片,或者使用 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
以更新 String
方法。当然,如果我们在同一个包中以这种方式设置了多个类型,只需一个命令就可以更新它们所有的 String
方法。
毫无疑问,生成的方法很丑陋。不过这没关系,因为人类不需要处理它;机器生成的代码通常都很丑陋。它在努力追求效率。所有名称都被压缩到一个单独的字符串中,这节省了内存(所有名称只有一个字符串头,即使有数百万个)。然后通过一种简单高效的技术,一个数组 _Pill_index
将值映射到名称。还要注意,_Pill_index
是一个 uint8
类型的数组(而不是切片;又省去一个头),它是足以覆盖所有值的最小整数类型。如果值更多,或者有负值,生成的 _Pill_index
类型可能会变为 uint16
或 int8
:哪种最合适就用哪种。
stringer
打印的方法所使用的方法取决于常量集的属性。例如,如果常量是稀疏的,它可能会使用 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
包中生成 Unicode 表,在 encoding/gob
中创建高效的数组编码和解码方法,在 time
包中生成时区数据,等等。
请创造性地使用 go
generate
。它旨在鼓励实验。
即使您不这样做,也请使用新的 stringer
工具为您的整数常量编写 String
方法。让机器来完成这项工作。
下一篇文章:Gopher Gala 是首个全球性 Go 黑客马拉松
上一篇文章:Go 1.4 发布了
博客索引