需要概述
go.uber.org/zap
日志包性能很好,然而用起来很不不便,尽管新版本增加了 global 办法,但依然顺当:zap.S().Info()
。
当初咱们的需要就是将 zap 的 sugaredLogger 封装成一个包,让它像 logrus
一样易用,间接调用包内函数:log.Info()
。
咱们只须要找到`SugaredLogger这个 type 领有的 Exported 办法,将其改为函数,函数体调用其同名办法:
func Info(args ...interface{}) { _globalS.Info(args)}
此处 var _globalS = zap.S()
,因为 zap.S()
每次调用都会调用 RWMutex.RLock()
,改为全局变量进步性能。
这个需要很简略,黏贴复制一顿 replace 就能够搞定,但这太蠢,咱们要用一种更 Geek 的形式:代码生成。
残缺代码:https://github.com/win5do/go-...
代码实现
要获取某个 type 的办法,大家可能会想到 reflect
反射包,然而 reflect 只能晓得参数类型,没法晓得参数名。所以这里咱们应用go/ast
间接解析源码。
获取 ast 语法树
办法可能扩散在包内不同 go 文件,所以必须解析整个包,而不是单个文件。
首先要找到 go.uber.org/zap
的源码门路,这里咱们极客到底,通过 go/build 包获取其在 gomod 中的门路,不必手动填写:
func getImportPkg(pkg string) (string, error) { p, err := gobuild.Import(pkg, "", gobuild.FindOnly) if err != nil { return "", err } return p.Dir, err}
解析整个 zap 包,拿到 ast 语法树:
func parseDir(dir, pkgName string) (*ast.Package, error) { pkgMap, err := goparser.ParseDir( token.NewFileSet(), dir, func(info os.FileInfo) bool { // skip go-test return !strings.Contains(info.Name(), "_test.go") }, goparser.Mode(0), // no comment ) if err != nil { return nil, errx.WithStackOnce(err) } pkg, ok := pkgMap[pkgName] if !ok { err := errors.New("not found") return nil, errx.WithStackOnce(err) } return pkg, nil}
遍历并批改 ast
遍历 ast,找到 SugaredLogger 的所有 Exported 办法:
func (v *visitor) Visit(node ast.Node) ast.Visitor { switch n := node.(type) { case *ast.FuncDecl: if n.Recv == nil || !n.Name.IsExported() || len(n.Recv.List) != 1 { return nil } t, ok := n.Recv.List[0].Type.(*ast.StarExpr) if !ok { return nil } if t.X.(*ast.Ident).String() != "SugaredLogger" { return nil } log.Printf("func name: %s", n.Name.String()) v.funcs = append(v.funcs, rewriteFunc(n)) } return v}
将办法 Recv 置空,变为函数,参数不变,函数 body 改为调用全局变量 _globalS
的同名办法,如果有返回值则须要 return 语句:
func rewriteFunc(fn *ast.FuncDecl) *ast.FuncDecl { fn.Recv = nil fnName := fn.Name.String() var args []string for _, v := range fn.Type.Params.List { for _, w := range v.Names { args = append(args, w.String()) } } exprStr := fmt.Sprintf(`_globalS.%s(%s)`, fnName, strings.Join(args, ",")) expr, err := goparser.ParseExpr(exprStr) if err != nil { panic(err) } var body []ast.Stmt if fn.Type.Results != nil { body = []ast.Stmt{ &ast.ReturnStmt{ // Return: Results: []ast.Expr{expr}, }, } } else { body = []ast.Stmt{ &ast.ExprStmt{ X: expr, }, } } fn.Body.List = body return fn}
上一步函数返回值中 zap.SugaredLogger
在指标包中须要改为 zap.SugaredLogger
,这里应用 type alias 简略解决一下,当然批改 ast 同样能做到:
// aliastype ( Logger = zap.Logger SugaredLogger = zap.SugaredLogger)
ast 转化为 go 代码
单个 func 的 ast 转化为 go 代码,应用 go/format
包:
func astToGo(dst *bytes.Buffer, node interface{}) error { addNewline := func() { err := dst.WriteByte('\n') // add newline if err != nil { log.Panicln(err) } } addNewline() err := format.Node(dst, token.NewFileSet(), node) if err != nil { return err } addNewline() return nil}
拼装成残缺 go file:
func writeGoFile(wr io.Writer, funcs []ast.Decl) error { // 输入Go代码 header := `// Code generated by log-gen. DO NOT EDIT.package log` buffer := bytes.NewBufferString(header) for _, fn := range funcs { err := astToGo(buffer, fn) if err != nil { return errx.WithStackOnce(err) } } _, err := wr.Write(buffer.Bytes()) return err}
这个程序是输入到了 os.Stdout,通过 go:generate
将其重定向到 zap_sugar_generated.go 文件中:
//go:generate sh -c "go run ./generator >zap_sugar_generated.go"
功败垂成,输入代码示例:
// Code generated by log-gen. DO NOT EDIT.package logfunc Desugar() *Logger { return _globalS.Desugar()}func Named(name string) *SugaredLogger { return _globalS.Named(name)}func With(args ...interface{}) *SugaredLogger { return _globalS.With(args)}func Debug(args ...interface{}) { _globalS.Debug(args)}func Info(args ...interface{}) { _globalS.Info(args)}func Warn(args ...interface{}) { _globalS.Warn(args)}func Error(args ...interface{}) { _globalS.Error(args)}func DPanic(args ...interface{}) { _globalS.DPanic(args)}func Panic(args ...interface{}) { _globalS.Panic(args)}func Fatal(args ...interface{}) { _globalS.Fatal(args)}// ......
即便之后 zap 包降级了,办法有增改,批改 gomod 版本再次执行 gernerate 即可一键同步,辞别手动复粘。
总结
Go 没法像 Java 那样做动静 AOP,但能够通过 go/ast 做代码生成,达成同样指标,而且不像 reflect 会影响性能和动态查看。用的好的话能够极大提高效率,更加自动化,缩小手工复粘,也就升高犯错概率。
已在很多明星开源我的项目里广泛应用,如:
- 代码编辑工具 gomodifytags: https://github.com/fatih/gomo...、
- Go 编译时依赖注入 Wire: https://github.com/google/wire
- K8S 源码:https://github.com/kubernetes...
Reference
https://github.com/kubernetes...
https://juejin.cn/post/684490...