注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
楔(xiē)子 最近写API CURD比较多,为了结构清晰,返回值需要统一错误码,所以在一个统一的errcode包中定义错误码常量,以及其错误信息.
如下图所示,由于常量是导出字符 -> golint 检测需要编写注释 -> 注释信息其实就是错误信息,已经在下文的msg map[int]string中定义,如果在写就得写两遍
不写,就满屏波浪线,不能忍!
写了,就得Copy一份,还不利于维护,不能忍!
能不能只写一份注释,剩下的msg通过读取注释信息自动生成,将我们宝(hua)贵(diao)的生命,从这些重复繁杂无意义的劳动中解放出来。
为了实现这个伟大的目标, 需要以下两个关键的数据:
解析源代码获取常量与注释之间的关系 -> 🌲Go 抽象语法树: AST[3]  从 Go 源码生成 Go 代码 -> 👏 go generate[5]  👏 go generate golang在1.4版本中引入了go generate命令,常用于文件生成,例如在 Golang 官方博客[5] Stringer 可以为枚举自动实现Stringer的方法,从业务代码中解放出来
💻 命令文档 使用go help generate我们可以查看一下命令的帮助文档
1 2 3 ▶ go help  generate
解释很长,就不贴上来了,简要的概括一下:
参数说明
-run 正则表达式匹配命令行,仅执行匹配的命令(和go test -run类似) -v 打印  已被检索处理的文件。 -n 打印出将被执行的命令,此时将不真实执行命令 -x 打印已执行的命令 举个栗子
1 2 3 4 
go generate会扫描.go源码文件中的注释//go:generate command args..., 并且执行其命令,注意:
这些命令是为了更新或者创建 Go 源文件 command必须是可执行的指令,例如在 PATH 中或者使用绝对路径arg如果带引号会被识别成一个参数, 例如: //go:generate command "x1 x2", 这条语句执行的命令只有一个参数注释中//和go之间没有空格 go generate必须手动执行,如果想等着go build, go test, go run 命令执行的时候自动执行,可以洗洗睡了
为了让别人或者是 IDE 识别代码是通过go generate生成的,请在生成的代码中添加注释(一般放在文件开头)
1 2 
举个栗子:
go generate在执行的时候会自动注入以下环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 $GOARCH $GOOS $GOFILE $GOLINE $GOPACKAGE $DOLLAR 
🌰 Go 官方博客中给出的栗子 源文件: painkiller.go
1 2 3 4 5 6 7 8 9 10 11 12 13 package  painkillertype  Pill int const  (iota 
执行命令
生成文件: painkiller_stringer.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  painkillerimport  "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 ]]
从上面的 🌰,我们可以发现,在.go源文件中,添加了一行注释go:generate stringer -type=Pill, 执行命令go generate就调用stringer命令在同目录下生成了一个新的_stringer.go的文件
回想一下上文提到的需求,是不是感觉很类似,从 Go 源文件中,生成了一些不想重复写的业务逻辑
🌲 AST 回到前面的需求,我们需要从源代码中获取常量和注释之前的关系,这时就需要我们的 🌲AST 隆重登场了。
本文不对 AST 过多介绍,可以阅读参考资料中的 AST 标准库文档[3] [4] 
简要介绍一下 AST 包 基础的接口类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 type  Node interface  {type  Expr interface  {type  Stmt interface  {type  Decl interface  {
等会儿可能会用到的ValueSpec
1 2 3 4 5 6 7 8 type  ValueSpec struct  {
在 godoc[3] CommentMap 例子
1 2 type  CommentMap map [Node][]*CommentGroup
通过parse读取源码创建一个 AST
1 2 3 4 5 fset := token.NewFileSet() "src.go" , src, parser.ParseComments)if  err != nil  {panic (err)
从 AST 中新建一个CommentMap
1 cmap := ast.NewCommentMap(fset, f, f.Comments)
需求实现 1. 获取常量和注释的关联关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 file := os.Getenv("GOFILE" )var  comments = make (map [string ]string )nil , parser.ParseComments)for  node := range  cmap {if  spec, ok := node.(*ast.ValueSpec); ok && len (spec.Names) == 1  {0 ]if  ident.Obj.Kind == ast.Con {
2. 获取注释信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func  getComment (name string , group *ast.CommentGroup)  string var  buf bytes.Bufferfor  _, comment := range  group.List {"// %s" , name)))for  i, b := range  bytes {switch  b {case  '\t' , '\n' , '\r' :' ' return  string (bytes)
3. 生成代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 const  suffix = "_msg_gen.go" const  tpl = ` // Code generated by github.com/mohuishou/gen-const-msg DO NOT EDIT // {{.pkg}} const code comment msg package {{.pkg}} // noErrorMsg if code is not found, GetMsg will return this const noErrorMsg = "unknown error" // messages get msg from const comment var messages = map[int]string{ 	{{range $key, $value := .comments}} 	{{$key}}: "{{$value}}",{{end}} } // GetMsg get error msg func GetMsg(code int) string { 	var ( 		msg string 		ok  bool 	) 	if msg, ok = messages[code]; !ok { 		msg = noErrorMsg 	} 	return msg } ` func  gen (comments map [string ]string )  ([]byte , error) var  buf = bytes.NewBufferString("" )map [string ]interface {}{"pkg" :      os.Getenv("GOPACKAGE" ),"comments" : comments,"" ).Parse(tpl)if  err != nil  {return  nil , errors.Wrapf(err, "template init err" )if  err != nil  {return  nil , errors.Wrapf(err, "template data err" )return  format.Source(buf.Bytes())
总结 从一个简单的效率需求引申到go generate和ast的使用,顺便阅读了一下ast的源码,花费的时间其实可能是这个工具节约的时间的几倍了,但是收获也是之前没有想到的。
使用了这么久的go命令,详细的阅读了go help command的说明之后,发现之前可能连了解都算不上 标准库的godoc是最好的使用说明,第二好的是它的源代码 参考资料 go-const-msg 本文实现的源代码 
Golang Generate 命令说明与使用 
AST 标准库文档 
Go 的 AST(抽象语法树) 
GO 官方博客: Generating code 
关注我获取更新 猜你喜欢