注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
楔(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 usage: go generate [-run regexp] [-n] [-v] [-x] [build flags] [file.go... | packages] ...
解释很长,就不贴上来了,简要的概括一下:
参数说明
-run 正则表达式匹配命令行,仅执行匹配的命令(和go test -run
类似) -v 打印 已被检索处理的文件。 -n 打印出将被执行的命令,此时将不真实执行命令 -x 打印已执行的命令 举个栗子
1 2 3 4 go generate -v go generate -n ./...
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 ^// Code generated .* DO NOT EDIT\.$
举个栗子:
go generate
在执行的时候会自动注入以下环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 $GOARCH 系统架构: arm, amd64 等$GOOS 操作系统: linux, windows 等$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 ( Placebo Pill = iota Aspirin Ibuprofen Paracetamol Acetaminophen = Paracetamol )
执行命令
生成文件: 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] ,Go 的 AST(抽象语法树)[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 { Pos() token.Pos End() token.Pos }type Expr interface { Node exprNode() }type Stmt interface { Node stmtNode() }type Decl interface { Node declNode() }
等会儿可能会用到的ValueSpec
1 2 3 4 5 6 7 8 type ValueSpec struct { Doc *CommentGroup Names []*Ident Type Expr Values []Expr Comment *CommentGroup }
在 godoc[3] 的 Example 中可以发现有一个CommentMap 例子
1 2 type CommentMap map [Node][]*CommentGroup
通过parse
读取源码创建一个 AST
1 2 3 4 5 fset := token.NewFileSet() f, err := parser.ParseFile(fset, "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 ) fset := token.NewFileSet() f, err := parser.ParseFile(fset, file, nil , parser.ParseComments) checkErr(err) cmap := ast.NewCommentMap(fset, f, f.Comments)for node := range cmap { if spec, ok := node.(*ast.ValueSpec); ok && len (spec.Names) == 1 { ident := spec.Names[0 ] if ident.Obj.Kind == ast.Con { comments[ident.Name] = getComment(ident.Name, spec.Doc) } } }
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.Buffer for _, comment := range group.List { text := strings.TrimSpace(strings.TrimPrefix(comment.Text, fmt.Sprintf("// %s" , name))) buf.WriteString(text) } bytes := buf.Bytes() for i, b := range bytes { switch b { case '\t' , '\n' , '\r' : bytes[i] = ' ' } } 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("" ) data := map [string ]interface {}{ "pkg" : os.Getenv("GOPACKAGE" ), "comments" : comments, } t, err := template.New("" ).Parse(tpl) if err != nil { return nil , errors.Wrapf(err, "template init err" ) } err = t.Execute(buf, data) 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
关注我获取更新 猜你喜欢