2

尝试使用 go generate

 2 years ago
source link: https://liqiang.io/post/using-go-generate-with-go-a10df252?lang=ZH_CN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

尝试使用 go generate

@POST· 2022-08-25 22:23 · 30 min read

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

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

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



  1. [[email protected]]# go generate ./...

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



  1. [[email protected]]# //go:generate mygentool arg1 arg2 -on

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



  1. [[email protected]]# 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. [[email protected]]# 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. [[email protected]]# go get golang.org/x/tools/cmd/stringer
  2. [[email protected]]# go generate

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



  1. [[email protected]]# 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. [[email protected]]# 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 的结构。

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

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



  1. [[email protected]]# go get golang.org/x/tools/cmd/goyacc

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



  1. [[email protected]]# 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK