Go 1.4 新功能:go generate

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

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_indexuint8 的数组 (不是切片; 消除了一个头), 这是足以跨越值空间的最小整数. 如果有更多值或有负值, 则生成的 _Pill_index 类型可能更改为 uint16int8 效果最佳.

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 方法. 让机器完成工作.

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/generat...

译文地址:https://learnku.com/docs/go-blog/generat...

上一篇 下一篇
Summer
贡献者:1
讨论数量: 0
发起讨论 只看当前版本


暂无话题~