背景
errerr
// @Log()@Log
编写代码
go/ast
[1]直接查看 AST ,当然也可以本地进行调试。
.go
filepath.WalkwalkFnwalkFn.go
// walkFn 函数会对每个 .go 文件处理,并调用注解处理器func walkFn(path string, info os.FileInfo, err error) error { // 如果是文件夹,或者不是 .go 文件,则直接返回不处理 if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") { return nil }
// 将 .go 文件解析成 AST fileSet, file, err := parseFile(path) // 如果注解修改了内容,则需要生成新的代码 if logannotation.Overwrite(path, fileSet, file) { buf := &bytes.Buffer{} if err := format.Node(buf, fileSet, file); err != nil { panic(err) }
// 如果不需要替换,则生成到另一个文件 if !replace { lastSlashIndex := strings.LastIndex(path, "/") genDirPath :=path[:lastSlashIndex] + "/_gen/" if err := os.Mkdir(genDirPath, 0755); err != nil && os.IsNotExist(err){ panic(err) } path = genDirPath + path[lastSlashIndex+1:] }
if err := ioutil.WriteFile(path, buf.Bytes(), info.Mode()); err != nil { panic(err) } }
return nil}
遍历 AST
astutil.Applyimport
// Overwrite 会对每个 file 处理,运行注册的注解 handler ,并返回其是否被修改func Overwrite(filepath string, fileSet *token.FileSet, file *ast.File) (modified bool) { // 初始化处理本次文件所需的信息对象 info := &Info{ Filepath: filepath, NamedImportAdder: func(name string, path string) bool { return astutil.AddNamedImport(fileSet, file, name, path) }, }
// 遍历当前文件 ast 上的所有节点 astutil.Apply(file, nil, func(cursor *astutil.Cursor) bool { // 处理 log 注解 info.Node = cursor.Node() nodeModified, err := Handler.Handle(info) if err != nil { panic(err) }
if nodeModified { modified = nodeModified }
return true })
return}
识别注释注解
@Log()
func (h *handler) Handle(info *Info) (modified bool, err error) { // log 注解只用于函数 funcDecl, ok := info.Node.(*ast.FuncDecl) if !ok { return }
// 如果没有注释,则直接处理下一个 if funcDecl.Doc == nil { return }
// 如果不是可以处理的注解,则直接返回 doc := strings.Trim(funcDecl.Doc.Text(), "\t \n") if doc != "@Log()" { return } ...}
获取函数入参和出参
funcDecl.Type.Results_0_1...
// SetDefaultNames 给没有名字的 Field 设置默认的名称// 默认名称格式:_0, _1, ...// true: 表示至少设置了一个名称// false: 表示未设置过名称func SetDefaultNames(fields ...*ast.Field) bool { index := 0 for _, field := range fields { if field.Names == nil { field.Names = NewIdents(fmt.Sprintf("_%v", index)) index++ } }
return index > 0}
获取打印语句
log.Logger.WithContext(ctx).WithField("filepath", filepath).Infof(format, arg0, arg1)parser.ParseExpr(format, arg0, arg1)
// NewCallExpr 产生一个调用表达式// 待产生表达式:log.Logger.WithContext(ctx).Infof(arg0, arg1)// 其中:// funcSelector = "log.Logger.WithContext(ctx).Infof"// args = ("arg0", "arg1")// 调用语句:NewCallExpr("log.Logger.WithContext(ctx).Infof", "arg0", "arg1")func NewCallExpr(funcSelector string, args ...string) (*ast.CallExpr, error) { // 获取函数对应的表达式 funcExpr, err := parser.ParseExpr(funcSelector) if err != nil { return nil, err }
// 组装参数列表 argsExpr := make([]ast.Expr, len(args)) for i, arg := range args { argsExpr[i] = ast.NewIdent(arg) }
return &ast.CallExpr{ Fun: funcExpr, Args: argsExpr, }, nil}
defer
// NewFuncLitDefer 产生一个 defer 语句,运行一个匿名函数,函数体是入参语句列表func NewFuncLitDefer(funcStmts ...ast.Stmt) *ast.DeferStmt { return &ast.DeferStmt{ Call: &ast.CallExpr{ Fun: NewFuncLit(&ast.FuncType{}, funcStmts...), }, }}
修改函数体
至此我们已经获得了打印入参和出参的语句,接下来就是把他们放在原本函数体的最前面,保证开始和结束时执行。
toBeAddedStmts := []ast.Stmt{ &ast.ExprStmt{X: beforeExpr}, // 离开函数时的语句使用 defer 调用 NewFuncLitDefer(&ast.ExprStmt{X: afterExpr}),}
// 我们将添加的语句放在函数体最前面funcDecl.Body.List = append(toBeAddedStmts, funcDecl.Body.List...)
运行
为了测试我们的注释注解是否工作正确,我们使用如下代码进行测试:
package main
import ( "context" "logannotation/testdata/log")
func main() { fn(context.Background(), 1, "2", "3", true)}
// @Log()func fn(ctx context.Context, a int, b, c string, d bool) (int, string, string) { log.Logger.WithContext(ctx).Infof("#fn executing...") return a, b, c}
go run logannotation/cmd/generator /Users/idealism/Workspaces/Go/golang-log-annotation/testdata/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/_gen
package main
import ( "context" "logannotation/testdata/log")
func main() { fn(context.Background(), 1, "2", "3", true)}
func fn(ctx context.Context, a int, b, c string, d bool) (_0 int, _1 string, _2 string) { log.Logger.WithContext( ctx).WithField("filepath", "/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go").Infof("#fn start, params: %+v, %+v, %+v, %+v", a, b, c, d) defer func() { log.Logger.WithContext( ctx).WithField("filepath", "/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go").Infof("#fn end, results: %+v, %+v, %+v", _0, _1, _2) }()
log.Logger.WithContext(ctx).Infof("#fn executing...") return a, b, c}
ctxtraceIdlogrusHooksFields
INFO[0000]#fnstart,params:1,2,3,truefilepath=/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.goINFO[0000] #fn executing... INFO[0000] #fn end, results: 1, 2, 3 filepath=/Users/idealism/Workspaces/Go/golang-log-annotation/testdata/main.go
扩展
以上代码是一种简单方式地定制化处理注解,仅处理了打印日志这一逻辑,当然还存在更多扩展的可能性和优化。
•注册自定义注解(这样可以把更多重复逻辑抽出来,例如:参数校验、缓存等逻辑)•同时使用多个注解•注解解析成语法树,支持注解参数•生成的代码仅在需要时换行
Demo
[2]找到。
References
[1][2]