乐趣区

关于golang:利用-goast-语法树做代码生成

需要概述

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 同样能做到:

// alias
type (
    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 log

func 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…

退出移动版