概述

在 Go 里面,有个不成文的习惯,就是很多人喜欢用生成代码,大的例如项目的目录结构,grpc 的 stub code 都是用工具生成的,小的例如静态文件嵌入到代码里面,自动生成 enum 类型的 String 形式等,反正生成代码的模式总是可以看到。

其实,这或许和 Go 官方也是推崇这种方式有关,例如 Go 从 1.4 版本起就开始有 go generate 的功能,虽然我在 2 年多前就开始写这篇文章的草稿了,但是这么久其实也没怎么认真地去了解一下这个功能。最近因为尝试用这个功能,所以真正地了解一下 go genreate,并且在这篇文章中我尝试总结一下关于 go generate 的内容。

工作原理

go generate 的工作原理就是当你在命令行里面输入:

  1. [root@liqiang.io]# go generate ./...

的时候,go 在你的当前目录下查找包含 //go:generate 注释的地方,然后这个注释一般后面是跟着 command 的,例如这个 comment:

  1. [root@liqiang.io]# //go:generate mygentool arg1 arg2 -on

其实就跟你在这个目录执行:mygentool arg1 arg2 -on 一样,不过差别在于,你在本地执行不会有额外的元数据,但是,如果你通过 go generate 运行的话,go 默认会给你添加一些额外的属性,这些属性可以通过这个程序来验证(需要注意的是 generate 工具需要在你的 PATH 目录下,如果在当前目录,别忘了将当前目录加入到你的 PATH 环境变量中):

  1. [root@liqiang.io]# go generate ./...
  2. args[0]: mygentool
  3. args[1]: arg1
  4. args[2]: arg2
  5. args[3]: -on
  6. GO111MODULE=auto
  7. GOPATH=/xxxx/software/go/gopath
  8. GOROOT=/xxxx/software/go/goroot/go
  9. GOARCH=amd64
  10. GOOS=linux
  11. GOFILE=sample.go
  12. GOLINE=3
  13. GOPACKAGE=main
  14. cwd: /xxxx/blog_codes/golang/tools/generator/example1

可以看到,Go 默认帮我们传递了很多环境变量进去,例如这个 comment 是在哪个文件中的,在哪一行,包名是什么,然后你是在哪个目录执行的 go generate 命令。通过这些参数,我们就可以做很多有趣的事情。

stringer

现在来看一下 Go 官方博客提到的一个例子:Go Generate,这时一个 stringer 的例子,其实就是为 enum 类型添加一个 string 的方法:

  1. [root@liqiang.io]# cat example.go
  2. package painkiller
  3. //go:generate stringer -type=Pill
  4. type Pill int
  5. const (
  6. Placebo Pill = iota
  7. Aspirin
  8. Ibuprofen
  9. Paracetamol
  10. Acetaminophen = Paracetamol
  11. )

然后执行命令:

  1. [root@liqiang.io]# go get golang.org/x/tools/cmd/stringer
  2. [root@liqiang.io]# go generate

可以看到当前目录会多了一个文件:pill_string.go,然后尝试做一个测试:

  1. [root@liqiang.io]# cat pill_test.go
  2. func TestPill_String(t *testing.T) {
  3. var p painkiller.Pill
  4. if p.String() != "Placebo" {
  5. t.Fatalf("p should equal to Placebo")
  6. }
  7. }

可以发现 enum 类型就有了 String 方法,然后这个方法的返回值就是 Enum 的 String 值了。

通过查看 stringer 的代码,可以发现 stringer 就是一个可执行程序,然后支持的参数有这些:

  1. [root@liqiang.io]# cat stringer.go
  2. var (
  3. typeNames = flag.String("type", "", "comma-separated list of type names; must be set")
  4. output = flag.String("output", "", "output file name; default srcdir/<type>_string.go")
  5. trimprefix = flag.String("trimprefix", "", "trim the `prefix` from the generated constant names")
  6. linecomment = flag.Bool("linecomment", false, "use line comment text as printed text when present")
  7. buildTags = flag.String("tags", "", "comma-separated list of build tags to apply")
  8. )

它的实现就是通过 ast (ast.Inspect(file.file, file.genDecl))解析你的文件,然后找到指定的名称,遍历它的值,然后再将这些值合并成数组,最后够造出 stringer 的结构。

yacc

最后再来介绍一个高级的用法,就是通过 go generate 调用 yacc 自动生成代码,这其实是一种元编程的思想,就是我们通过指定一个元数据,然后通过这个元数据来创建出代码(例如创建出 struct,然后这个 struct 自动生成了很多内置的方法,有点类似与 proto -> go 代码,但是更为地高级,功能更丰富)关于元编程的知识,我曾经写过一个关于 python 元类的文章,不妨可以一看:python元类浅析

但是,基于本人对 yacc 也熟悉,我只知道这是编译类的工具,但是,平时也没有去了解或者使用,所以这里还是以官方文档中的用法为主进行介绍,首先先安装 Go 版本的 yacc :

  1. [root@liqiang.io]# go get golang.org/x/tools/cmd/goyacc

然后编辑你的 yacc 文件,例如我从 repo 中 copy 了一个,然后创建 go 文件,包含 go generate 命令:

  1. [root@liqiang.io]# cat main.go
  2. package main
  3. //go:generate goyacc -o calc.go calc.y
  4. func main() {
  5. }

然后运行 go generate ./... 命令,就发现在本地多了一个文件 calc.go,我对 yacc 不熟悉,但是从代码来看像是 clac 定义的语法规则,应该是用来解析特定规则的语法的,因为我不擅长,所以就不展开了,但是功能还是这样用的,没有脱离基本的操作方法。

示例代码

本文的所有代码都可以在这个 repo 中找到:https://github.com/liuliqiang/blog_codes/tree/master/golang/tools/generator

参考项目