Go 1.4 新功能:go generate
Rob Pike
2014 年 12 月 22 日
通用计算的一个特性 (图灵完整性) 是计算机程序可以编写计算机程序。这是一个很有力的想法,尽管它经常发生,但却没有得到应有的重视。例如,这是编译器定义的很大一部分。这也是 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
方法.
毫无疑问,生成的方法很丑陋。没关系,因为人类不需要对此进行操作。机器生成的代码通常很难看。努力提高效率。所有名称都被粉碎成一个字符串,从而节省了内存 (所有名称只有一个字符串头,即使有成千上万个名称也是如此). 然后数组 _Pill_index
通过一种简单有效的技术从值映射到名称。还要注意 _Pill_index
是 uint8
的数组 (不是切片;消除了一个头), 这是足以跨越值空间的最小整数。如果有更多值或有负值,则生成的 _Pill_index
类型可能更改为 uint16
或 int8
效果最佳.
stringer
打印的方法使用的方法根据常量集的属性而有所不同。例如,如果是常量,则可能使用映射。这是一个基于常量集的琐碎示例,该常量集表示 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
方法。让机器完成工作.
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: