Golang generate 草案

简介

既有的 go build 命令自动构造 go 程序。但是有时候我们需要做一些预处理。例如以下原因:

  • yacc:从 .y 文件生成 .go 文件。
  • protobufs:从 protocol buffer 定义文件(.proto)生成 .pb.go 文件。
  • Unicode:从 UnicodeData.txt 生成 Unicode 表。
  • HTML:将 HTML 文件嵌入到 go 源码 。
  • bindata:将形如 JPEG 这样的文件转成 go 代码中的数组。

我们还可以设想一些其它的处理步骤:

  • 为类似枚举常量这样的类型生成 String() string 方法。
  • 宏:为既定的泛型包生成特定的实现,例如用于 ints 的 sorts.Ints 。

这个草案为这些处理需求提供了平滑的设计。

非既定目标的

这个草案不是为了实现一个类似 unix make 这样的通用构建工具而设计。我们谨慎的考察了所有依赖分析。这个工具只做我们要做的,别无它求。
然而,我们希望它至少能够代替 go 代码库中出现的大量 make。

设计

草案中包括两个基本元素,一个是 go 命令调用的子命令,称为 go generate ,一个是 go 源码中控制生成的指令。
运行 go generate 的时候,它扫描 go 源码文件,查找那些指令,逐个执行生成器,输出新的 go 源码。 go generate tool 也可以设置 build 标记为 “generate”,这样 go generate 会校验文件,但是 build 的时候忽略它们。

用法:

 go generate [-run regexp] [file.go...|packagePath...]

(常用的可选参数有 -x, -n, -v 和 -tags ),如果参数是 pacakge,每一个包的每个 go 源码文件都会扫描是否包含 generate 指令,每一个指令都会执行对应的 generate 行为;如果是文件,它必须是 go 源码文件,只有指定文件中的指令被执行。没有给定参数的话,generate 过程作用于当前目录的 go 源码文件。
标记 -run 接受正则表达式,类似 test 子命令。它只运行匹配正则表达式的指令。
生成指令可能出现在 go 源码文件的任意位置,生成工具会按顺序串行(非并行)处理。每个指令是一行以 // 开头的注释,语法为:
go

 //go:generate command arg...

上例中的 command 指要用来运行的 generator(诸如 yacc),相当于在此运行一个可执行程序;他必须在 shell 的path 中(如gofmt)或有同等效用(/usr/you/bin/mytool)。它在 package 目录中运行。
运行时传递给 generator 的参数是空格分割的 tokens(或者双引号界定的字符串)。任意系统变量,都可以用 Shell 风格的变量表达式,如 $HOME 。以及, $GOFILE 指向包含指令的文件名。(我们或许还需要 $GOPACKAGE 这样的特殊变量,这样在运行 generate 的时候,我们可以获知当前的shell环境)。没有其它的处理过程,包括全局的过程等,都没有。
如果某个 generator 以错误状态退出,后续的 generator 就不会运行。
举个例子,我们假设一个名为“my/own/gopher”的包,在文件 gopher.y 中包含了 yacc 语句。在 main.go(不是 gopher.y) 中我们写入指令:
go

 //go:generate yacc -o gopher.go gopher.y

(上述yacc代码的具体含义我们后面的章节再讨论。)一旦我们需要更新 generate 文件,只需要在 shell 中调用:
shell

 % go generate my/own/gopher

或者,如果我们已经在源码目录中:
shell

 % go generate

如果我们确定只要执行 yacc generator,可以执行:
shell

 % go generate -run yacc

如果我们修正了 yacc 代码中的 bug ,想要更新代码树中所有 yacc 生成的文件,我们可以运行:
shell

 % go generate -run yacc all

对于包开发者来说典型的 generate 工作循环可能是这样的:
shell

 % edit …
 % go generate
 % go test

一旦工作完成,作者把生成的文件提交到代码库,他们在客户端就可以用 go get 获取。
shell

 % git add *.go
 % git commit

 

命令

当然,yacc 程序还不是正式版(设计草案发布早于 go 1.4 beta1——译注),但是可以用如下方式从命令行调用:
shell

 go tool yacc args…

为了方便使用没有出现在 $PATH 中的 yacc 之类的工具,可以使用一些复杂的访问方法,或者用扩展标记定义,或其它方法封装。有一个特殊的指令用于定义命令的简写。在 go:generate 指令后面加 “-command” 关键字/标记即可生成其定义。在 generator 运行的过程中这一行就会替换掉命令的名字。因此我们先用这个指令,把 “yacc” 定义为一个 generator 命令,就可以用“go tool yacc”运行它。
go

 //go:generate -command yacc go tool yacc

然后文件中随后使用 yacc 的可以写为如下形式:
go

 //go:generate yacc -o gopher.go gopher.y

它会被转译为:
shell

 go tool yacc -o gopher.go gopher.y

运行。

讨论

这样的设计并不寻常,但是背后也遵循若干设计机制。
首先,go generate 设计为包作者使用,而非包的使用者。包的作者生成所需的go文件,将其包含在包中。用户只需 go get 或 go build。由 go generate 进行的泛型不是构建的一部分,而是包作者的工具。这免去了 go build 所需的复杂的依赖分析。
其次,go build 永远不需要关注包用户是否引发了自动的生成过程。 Generator 只有在显式调用时才会执行。
再者,包作者在使用生成器的过程中有很大的自由(这是本草案的一个关键优势),但是用户不应该干预预处理。举个简单的例子,假设一个 shell 脚本,它不会运行在 windows 上。重要的是在用户看不到的情况下生成过程被破坏了,它本应该仅由包作者调用。
最后,它必须良好的兼容既有的 go 命令。这就意味着它只能作用于go源码文件和包。这就是为什么指令写在 go 文件而非——例如——yacc 生成器的 .y 文件里。
设想包作者可能会期待用户运行生成过程,但是这首先要确保用户能够运行生成器。而 go get并不运行预处理器,这就要求作者提供安装手册。
分类:

示例

这里给出一些假设的例子,而现实中一切皆有可能。

字符串方法

我们希望为一个命名常量类型生成字符串方法。我们写了一个工具,称之为 strmeth,它逐个读取常量类型及其值,输出包含该类型定义的完整 go 源码文件。
在我们的 go 文件 main.go 中,我们按如下方法修饰每一个常量定义(我们用下划线标注,这样生成指令不会出现在文档注释中)。

#go
//go:generate strmeth Day -o day_string.go $GOFILE
// Day represents the day of the week
 type Day int
 const (
 Sunday Day = iota
 Monday
 …
 )

生成器 strmeth 解析 go 代码,找到其中的 Day 类型,及其常量,为其编写 String() string 方法。对于用户,字符串方法的生成细节不重要,只要运行 go generate。

Yacc

如前所示,我们定义了一个定制命令:

#go
//go:generate -command yacc go tool yacc

然后在 main.go 的某处我们写下:

#go 
//go:generate yacc -o foo.go foo.y

Protocol buffers

这个处理过程跟 yacc 一致。在 main.go 中,我们为每一个 protocol buffer 文件写下如下一行:

#go
//go:generate protoc -go_out=. file.proto

借助 protoc 的运行方式,我们可以在一个 .pb.go 文件中生成多个 proto 定义。

#go
//go:generate protoc -go_out=. file1.proto file2.proto

因为没有全局过程,我们不能指定 *.proto ,但是这是刻意设计的,以简化和确定依赖。
Protoc 程序必须运行在代码树的根,我们需要为它指定 -cd 选项或用某种方式封装起来。
Binary data
有个工具可以把二进制数据变成字节数组写进go文件里,肯定可以省些事儿。那么,在go源码中我们可以这样写:

#go
 //go:generate bindata -o jpegs.go pic1.jpg pic2.jpg pic3.jpg

这也说明了要在 go 源码中写 generate 注释的原因:没有什么好办法可以把它们注入到二进制文件里。
排序
我们假设有一个通用排序实现,只要自动重写一些类似宏的定义,就可以适用于一些有特定排序特征的类型。为此,我们写了一个 sort.go 文件,包含完整的排序实现。它基于一个明确的类型名 TYPE,但是这个类型实际上不存在。在这个文件中,我们给出构建标记,使其永远不会被编译(类型不存在,所以实际上这个源码文件没有完成),但是它可以用 generate 处理:

#go
// +build generate

然后我们为每个希望支持定制排序的类型编写一个生成指令:

#go
//go:generate rename TYPE=int
//go:generate rename TYPE=strings

或者:

#go
//go:generate rename TYPE=int TYPE=strings

重命名处理器可以是 gofmt -r 的简单封装,也可以是一个 shell script。
这个草案有很多可能,关于预处理时期代码生成也会有很多有趣的体验。

转载请注明来源:新一 » Golang generate 草案

赞 (0) 评论 (0) 分享 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址