Go AST
礼物说最近我司在拆分微服务,在迁移过程中重复工作较多,例如:
- 某个文件中的函数批量转换成proto文件
- 函数依赖的结构体转化成proto message
- 生成pb结构体与业务结构体的互相转换的工具
于是,一个idea诞生了,这就是func2pb.
而本文意在阐述如何解析Go AST语法树去完成某些特定任务。
获取 AST 文件
我们使用go/parser
和go/token
两个标准库.
// 文件路径
var path string
, err := ioutil.ReadFile(path)
dataif err != nil {
return
}
:= token.NewFileSet()
fset , err := parser.ParseFile(fset, file, string(data), parser.ParseComments) fast
至此我们可以获取一个go/ast
中的file
结构体
type File struct {
*CommentGroup // associated documentation; or nil
Doc .Pos // position of "package" keyword
Package token*Ident // package name
Name []Decl // top-level declarations; or nil
Decls *Scope // package scope (this file only)
Scope []*ImportSpec // imports in this file
Imports []*Ident // unresolved identifiers in this file
Unresolved []*CommentGroup // list of all comments in the source file
Comments }
获取 AST 中的函数
看到上述的结构体可能会很困惑,不应该有一个字段是Functions
或者Structs
么?其实所有的信息都在Decls
中。我们可以通过断言fn, isFunc := decl.(*ast.FuncDecl); isFunc
获取ast.FuncDecl
。
// A FuncDecl node represents a function declaration.
struct {
FuncDecl *CommentGroup // associated documentation; or nil
Doc *FieldList // receiver (methods); or nil (functions)
Recv *Ident // function/method name
Name *FuncType // function signature: type and value parameters, results, and position of "func" keyword
Type *BlockStmt // function body; or nil for external (non-Go) function
Body }
从这个结构体我们就可以很清晰的获取:
- 注释
Doc
- Receiver
Recv
- 函数名
Name
- 入参
Type.Params
- 出参
Type.Results
获取 AST 中的结构体
而获取结构体相对而言会更复杂。我们需要先将Decls
断言genDecl, isGenDecl := decl.(*ast.GenDecl);
,获取GenDecl
struct {
GenDecl *CommentGroup // associated documentation; or nil
Doc .Pos // position of Tok
TokPos token.Token // IMPORT, CONST, TYPE, or VAR
Tok token.Pos // position of '(', if any
Lparen token[]Spec
Specs .Pos // position of ')', if any
Rparen token}
再GenDecl.Specs
断言typeSpec, isTypeSpec := spec.(*ast.TypeSpec);
,获取TypeSpec
// A TypeSpec node represents a type declaration (TypeSpec production).
struct {
TypeSpec *CommentGroup // associated documentation; or nil
Doc *Ident // type name
Name *FieldList // type parameters; or nil
TypeParams .Pos // position of '=', if any
Assign token// *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes
Type Expr *CommentGroup // line comments; or nil
Comment }
最终断言TypeSpec
为structType, isStruct := typeSpec.Type.(*ast.StructType);
,获取StructType
// A StructType node represents a struct type.
struct {
StructType .Pos // position of "struct" keyword
Struct token*FieldList // list of field declarations
Fields bool // true if (source) fields are missing in the Fields list
Incomplete }
杂项
关于Go AST的解析强烈建议使用ChatGPT辅助生成。里面逻辑非常复杂。当我们获取到我们想要的内容后,我们可以运用一些辅助工具去生成我们的小工具。
生成命令行
这里为选用的是github.com/urfave/cli.具体不在赘述
生成模版文件
我们使用text/template
去生成模版文件,同时还可以注入一些函数template.FuncMap
比如转驼峰/字符串替换等等。例子如:
const tmplPB = `syntax = "proto3";
// TODO fill it
package xxx;
service {{ .Sn }} { {{ range .Funcs }}
{{ if commentnotempty .Comment }} {{ comment .Comment }}{{ end }} rpc {{ .Name }}({{ .Name }}Req) returns ({{ .Name }}Resp);{{ end }}
}
{{ range .Funcs }}
message {{ .Name }}Req {{ "{" }}{{ range $index, $element := .In }}
{{ replace .Typ }} {{ rename .Name }} = {{$index | add 1}};{{ end }}
}
message {{ .Name }}Resp {
int32 code = 1;
string msg = 2;
{{ .Name }}Data data = 3;
}
message {{ .Name }}Data {{ "{" }}{{ range $index, $element := .Out }}
{{ replace .Typ }} {{ rename .Name }} = {{$index | add 1}};{{ end }}
}
{{ end }}
{{ range .Structs }}
{{ if commentnotempty .Comment }}{{ comment .Comment }}{{ end }}message {{ .Name }} { {{ range $index, $element := .Field }}
{{ if commentnotempty .Comment }} {{ comment .Comment }}{{ end }} {{ replace .Typ }} {{ rename .Name }} = {{$index | add 1}};{{ end }}
}
{{ end }}
`
总结
用一些工具思维去解决一些定势问题,会有不少收获。