[Go] 译文 Generating code - The Go Blog

原文地址

Rob Pike 22 December 2014

通用计算有一个特性名为“图灵完整性”,他意味着计算机程序可以由其他计算机程序编写完成。这是一个很厉害想法,但却没有得到应得的欣赏,计算机程序可以由其他计算机程序编写这种事情经常在身边发生。这其实是编译器的重要组成部分,举个例子看下 go test 命令是如何工作的:它首先扫描需要被测试的 package,然后生成为该 package 定制的测试代码,并编译执行。现代计算机已经很快了,所以上面听起来复杂的步骤执行起来只需要不到一秒。

计算机程序编写计算机程序其实是有很多案例的。 譬如:Yacc 它读取输入的语法描述,然后生成解析语法的程序。 protocol buffer 的编译器也是类似,它读取接口描述并生成相应的结构定义以及函数等相关代码。又或者很多配置工具是这样工作的:为本地内容检查 metadata 、 environment 及相关的脚手架。

可以看出程序编写程序是软件工程领域里面非常重要的一部分。

一般我们会将 Yacc 集成到构建工具中,譬如 Make,这样我们便可以控制相关过程细节。如果想在 go tool 中使用 Yacc 是非常困难的,因为 go 的 go tool 相关工具只能从 go 源代码获取所需信息,并没有能够动态运行 Yacc 的相关机制功能。

在最新的 release 版本 1.4 中,这个问题得到解决,该版本新增了可以方便使用类似 Yacc 等相关工具的一个命令。这个命令便是 go generate,它的工作机制为:扫描包含通过注释指定的 generate 代码并执行。因为不包含相关依赖的分析过程,所以这个命令并不是 go build 的一部分,一般在 go build 执行前执行。这个功能是为 go 的开发者设计的,而不是一个客户端功能。

go generate 命令非常容易使用,做里举一个如何使用 Yacc 生成代码的例子热热身。

首先,安装 Go’Yacc tool:

go get golang.org/x/tools/cmd/goyacc

假设你有一个描述了新语言的语法文件叫做 gopher.y。一般情况下如果需要生成实现该语法的 go 代码,你需要执行该语句:

goyacc -o gopher.go -p parser gopher.y

-o 参数表述输出文件名,-p 参数表示 package 名。

我们可以通过 go generate 去驱动这个进程,在当前目录下的任意非生成文件的任意位置加上下面的注释:

//go:generate goyacc -o gopher.go -p parser gopher.y

上面这个包含特定前缀的注释便是 go generate 的识别内容。这个注释必须出现在一行的开始处,并且 // 之间不能有空格,//go:generate 之间也不能有空格。除了这些,剩下的便是你需要 go generate 替你执行的命令部分。

接着我们回到代码目录执行命令:

$ 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 tool 内置的,所以非常适合 go 的生态系统。为什么说它是为开发者设计的而不是一个客户端使用的工具呢,因为它所执行的程序可能在目标机器上并不可用。而且如果它需要通过 go get 获取一些 package 的话,生成的内容完成测试后合入项目版本中,客户端才能用。

接着我们尝试些其他事情,stringergolang.org/x/tools 项目下的新程序,它和前面 go generate 的案例不同。它能够自动生成关于常量集合的 string 方法,它并没有随着 release 版本发布,但是安装很容易:

$ 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)
}

当然,还有其他的实现方式,我们可以使用 string slice 来持有 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

除此之外,如果你在当前 package 里的多个定义使用 stringer,一样是上面一个命令完成所有的定义的 String 方法更新。

虽然生成的代码有点丑,但是好在你不需要去编辑它,一般而言,机器生成的代码都是挺丑的,但是能提高工作的效率。通过将所有的字符串合在一起,这样可以有效的节省内存。_Pill_index 是一个数组,用于映射具体的 name,这是一种非常简单高效的技术。_Pill_index 是一个 uint8 类型的数组,而不是 slice,uint8 为足够这里使用的最小整数,如果有更多的值,可能会提升到 uint16 或则 int8,具体视那种工作的更好。

如果你的常量集是稀疏类型的,则生成的代码可能会使用 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 package 中的 Unicide 表,encoding/gob 里高效编解码函数, time package 里的时间零值,等等。

通过 go generate 开始你的创造之旅吧,再不济也尝试使用 stringer 让机器代替你去生成那些整形常量的 String 函数吧。


tangzixiang