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
)
为了调试,我们希望这些常量能够很好地打印自己,这意味着我们需要一个具有以下签名的方法:
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
方法。
毫无疑问,生成的 method 很丑。不过这没关系,因为人类不需要处理它;机器生成的代码通常很丑。它努力高效。所有名称都被合并到一个字符串中,这节省了内存(所有名称只有一个字符串头,即使有数百万个名称)。然后,一个数组 _Pill_index
通过一个简单而高效的技术将值映射到名称。还要注意,_Pill_index
是一个 uint8
类型的数组(而不是切片;又减少了一个头),它是跨越值空间的最小整数。如果还有更多值,或者存在负值,则 _Pill_index
的生成类型可能会更改为 uint16
或 int8
:无论哪种最有效。
stringer
打印的方法使用的方法会根据常量集的属性而异。例如,如果常量是稀疏的,它可能会使用一个 map。以下是一个基于表示 2 的幂的常量集的简单示例
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 发布
博客索引