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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
推荐文章: