概述
在 Go 里面,有个不成文的习惯,就是很多人喜欢用生成代码,大的例如项目的目录结构,grpc 的 stub code 都是用工具生成的,小的例如静态文件嵌入到代码里面,自动生成 enum 类型的 String 形式等,反正生成代码的模式总是可以看到。
其实,这或许和 Go 官方也是推崇这种方式有关,例如 Go 从 1.4 版本起就开始有 go generate 的功能,虽然我在 2 年多前就开始写这篇文章的草稿了,但是这么久其实也没怎么认真地去了解一下这个功能。最近因为尝试用这个功能,所以真正地了解一下 go genreate,并且在这篇文章中我尝试总结一下关于 go generate 的内容。
工作原理
go generate 的工作原理就是当你在命令行里面输入:
[root@liqiang.io]# go generate ./...
的时候,go 在你的当前目录下查找包含 //go:generate
注释的地方,然后这个注释一般后面是跟着 command 的,例如这个 comment:
[root@liqiang.io]# //go:generate mygentool arg1 arg2 -on
其实就跟你在这个目录执行:mygentool arg1 arg2 -on
一样,不过差别在于,你在本地执行不会有额外的元数据,但是,如果你通过 go generate
运行的话,go 默认会给你添加一些额外的属性,这些属性可以通过这个程序来验证(需要注意的是 generate 工具需要在你的 PATH 目录下,如果在当前目录,别忘了将当前目录加入到你的 PATH 环境变量中):
[root@liqiang.io]# go generate ./...
args[0]: mygentool
args[1]: arg1
args[2]: arg2
args[3]: -on
GO111MODULE=auto
GOPATH=/xxxx/software/go/gopath
GOROOT=/xxxx/software/go/goroot/go
GOARCH=amd64
GOOS=linux
GOFILE=sample.go
GOLINE=3
GOPACKAGE=main
cwd: /xxxx/blog_codes/golang/tools/generator/example1
可以看到,Go 默认帮我们传递了很多环境变量进去,例如这个 comment 是在哪个文件中的,在哪一行,包名是什么,然后你是在哪个目录执行的 go generate
命令。通过这些参数,我们就可以做很多有趣的事情。
stringer
现在来看一下 Go 官方博客提到的一个例子:Go Generate,这时一个 stringer 的例子,其实就是为 enum 类型添加一个 string 的方法:
[root@liqiang.io]# cat example.go
package painkiller
//go:generate stringer -type=Pill
type Pill int
const (
Placebo Pill = iota
Aspirin
Ibuprofen
Paracetamol
Acetaminophen = Paracetamol
)
然后执行命令:
[root@liqiang.io]# go get golang.org/x/tools/cmd/stringer
[root@liqiang.io]# go generate
可以看到当前目录会多了一个文件:pill_string.go,然后尝试做一个测试:
[root@liqiang.io]# cat pill_test.go
func TestPill_String(t *testing.T) {
var p painkiller.Pill
if p.String() != "Placebo" {
t.Fatalf("p should equal to Placebo")
}
}
可以发现 enum 类型就有了 String
方法,然后这个方法的返回值就是 Enum 的 String 值了。
通过查看 stringer 的代码,可以发现 stringer 就是一个可执行程序,然后支持的参数有这些:
[root@liqiang.io]# cat stringer.go
var (
typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
trimprefix = flag.String("trimprefix", "", "trim the `prefix` from the generated constant names")
linecomment = flag.Bool("linecomment", false, "use line comment text as printed text when present")
buildTags = flag.String("tags", "", "comma-separated list of build tags to apply")
)
它的实现就是通过 ast (ast.Inspect(file.file, file.genDecl)
)解析你的文件,然后找到指定的名称,遍历它的值,然后再将这些值合并成数组,最后够造出 stringer 的结构。
yacc
最后再来介绍一个高级的用法,就是通过 go generate 调用 yacc 自动生成代码,这其实是一种元编程的思想,就是我们通过指定一个元数据,然后通过这个元数据来创建出代码(例如创建出 struct,然后这个 struct 自动生成了很多内置的方法,有点类似与 proto -> go 代码,但是更为地高级,功能更丰富)关于元编程的知识,我曾经写过一个关于 python 元类的文章,不妨可以一看:python元类浅析 。
但是,基于本人对 yacc 也熟悉,我只知道这是编译类的工具,但是,平时也没有去了解或者使用,所以这里还是以官方文档中的用法为主进行介绍,首先先安装 Go 版本的 yacc :
[root@liqiang.io]# go get golang.org/x/tools/cmd/goyacc
然后编辑你的 yacc 文件,例如我从 repo 中 copy 了一个,然后创建 go 文件,包含 go generate 命令:
[root@liqiang.io]# cat main.go
package main
//go:generate goyacc -o calc.go calc.y
func main() {
}
然后运行 go generate ./...
命令,就发现在本地多了一个文件 calc.go
,我对 yacc 不熟悉,但是从代码来看像是 clac 定义的语法规则,应该是用来解析特定规则的语法的,因为我不擅长,所以就不展开了,但是功能还是这样用的,没有脱离基本的操作方法。
示例代码
本文的所有代码都可以在这个 repo 中找到:https://github.com/liuliqiang/blog_codes/tree/master/golang/tools/generator