写点什么

5 分钟搞定 Golang 自定义代码分析器

作者:俞凡
  • 2025-01-20
    上海
  • 本文字数:4730 字

    阅读完需:约 16 分钟

本文介绍了如何利用 Go 的 analysis 包提高代码质量,通过构建自定义分析器并与 golangci-lint 集成,详细说明了 analysis 包的使用方法和如何定义分析器,以及如何将自定义分析器集成到 golangci-lint 中。原文:Enhancing Code Quality with Go’s Analysis Package



Go 语言因其简洁的语法和极高的性能而广受欢迎。随着使用范围越来越广,遵循编码标准和一致性就越来越重要。为了实现这一目标,就需要借助静态代码分析工具的帮助,比如著名的 golangci-lint


在研究 golangci-lint 和 Go 官方指南的过程中,我发现了一个强大的工具 -- golang.org/x/tools/go/analysis 软件包,它可以帮助定制代码分析器。

分析什么内容

golang.org/x/tools/go/analysis 软件包为 Go 提供了一个静态分析框架,支持可传递分析(分析器的输出可作为其他分析器的输入),并使开发人员能够定制用于静态代码检查的工具。这些工具可以集成到 Go 工具链中,例如可以通过 go vet 命令运行。

主要 API
  • 核心结构 Analyzer 也是一个静态分析器。每个 Analyzer 都有名称、描述和 Run 函数作为前置条件。


type Analyzer struct {  Name             string  Doc              string  Flags            flag.FlagSet  Run              func(*Pass) (interface{}, error)  RunDespiteErrors bool  ResultType       reflect.Type  Requires         []*Analyzer  FactTypes        []Fact}
复制代码


Analyzer 类似于 Cobra Go CLI 命令,包括用于定义参数的 Flags 和用于执行 Pass 的主 Run 函数。ResultType 是该 Analyzer 的执行结果,可被其他分析器使用。Requires 是当前 Analyzer 依赖的一组分析程序。



type Pass struct {  Analyzer *Analyzer // 当前分析器的标识
// 语法和类型信息 Fset *token.FileSet // 文件位置信息 Files []*ast.File // 每个文件的抽象语法树 OtherFiles []string // 此包的非go文件的名称 IgnoredFiles []string // 包中被忽略的源文件名称 Pkg *types.Package // 关于包的类型信息 TypesInfo *types.Info // 关于语法树的类型信息 TypesSizes types.Sizes // 计算类型大小的函数 ExportObjectFact func(types.Object, Fact) ImportObjectFact func(types.Object, Fact) bool
...}
复制代码


  • Fact 是一种中间事实,可用于在不同检查器之间传递信息。这些信息通常是对软件包的断言,例如某个类型是否实现了某个接口,或者某个函数是否总是返回非零错误,这些信息被附加到语法树节点上,供后续分析使用。


type Fact interface { AFact() // dummy method to avoid type errors}
复制代码


使用 Fact 通常有 4 个步骤:创建、注册、保存和查看。


下面是一个简单的分析器,可以遍历文件找到相应的 Function 语句,并将其保存为 Fact 供以后查看。


type myFact struct {    Message string}
func (f *myFact) AFact() {}
var Analyzer = &analysis.Analyzer{ Name: "example", Doc: "example analyzer that uses Facts and Passes", Run: run, FactTypes: []analysis.Fact{(*myFact)(nil)},}
func run(pass *analysis.Pass) (interface{}, error) { for _, file := range pass.Files { for _, decl := range file.Decls { if funcDecl, ok := decl.(*ast.FuncDecl); ok { // add a Fact fact := &myFact{Message: "This is a function"} // export the fact pass.ExportObjectFact(funcDecl.Name, fact) } } } checkFacts(pass) return nil, nil}
func checkFacts(pass *analysis.Pass) { for _, file := range pass.Files { for _, decl := range file.Decls { if funcDecl, ok := decl.(*ast.FuncDecl); ok { var fact myFact // import the fact if pass.ImportObjectFact(funcDecl.Name, &fact) { fmt.Printf("Function %s: %s\n", funcDecl.Name.Name, fact.Message) } } } }}
func main() { singlechecker.Main(Analyzer)}
复制代码


正常情况下,我们很少使用 Facts。每个检查器都相互独立,只关注自己的任务,并使用 Pass.Reportf 方法报告错误。下面展示了如何检查没有未使用变量的 go 文件。


package main
import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/singlechecker" "golang.org/x/tools/go/ast/inspector")
var Analyzer = &analysis.Analyzer{ Name: "unusedvar", Doc: "checks for unused variables in functions", Run: run,}
func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{ (*ast.FuncDecl)(nil), }
inspect.Preorder(nodeFilter, func(n ast.Node) { fn, _ := n.(*ast.FuncDecl) if fn.Body == nil { return // interface }
for _, stmt := range fn.Body.List { switch stmt := stmt.(type) { case *ast.AssignStmt: for _, lhs := range stmt.Lhs { if ident, ok := lhs.(*ast.Ident); ok && !ident.Used() { pass.Reportf(ident.Pos(), "unused variable: %s", ident.Name) } } } } })
return nil, nil}
func main() { singlechecker.Main(Analyzer)}
复制代码


在分析器中,我们首先定义一个 Analyzer 结构实例,然后指定分析器名称、描述和 Run 函数。在 Run 函数中,使用 inspector 过滤函数声明节点,检查每个函数中的变量分配情况,如果变量未被使用,则通过 pass.Reportf 方法报错。

AST 树

analysis 包本身只提供了代码框架,侧重于代码分析操作、结果计算和交付等上层抽象,并提供了链式组合接口。如上述代码所示,ast.Node 发起的遍历发生在 Preorder 函数中。因此,理解 Golang 的 ast 树有助于创建分析器。


在 ast 树中,节点(Node)是最基本的接口,由此衍生出越来越多的具体类型。



// different types of expr// Identvar x// BasicLit42"hello"// BinaryExpra + b// CallExprfmt.Println("hello world!")// IndexExprarr[1]m["key"]// SliceExprs[1:5]// TypeAssertExprx.(int)// UnaryExpr&x!b// CompositeLit[]int{1,2,3}
复制代码




处理的功能包括:


  • filter 方法:FilterDeclFilterFileFilterPackage 可以通过传入的过滤器过滤不同节点。请看如下示例,该示例通过遍历和过滤来获取没有 Function 声明的节点。


// 定义 Filter 函数来过滤掉函数声明filterFunc := func(d ast.Decl) bool {    _, ok := d.(*ast.FuncDecl)    return !ok  }
// 过滤文件ast.FilterFile(node, filterFunc)
复制代码



// 打印非 nil 的所有节点ast.Fprint(os.Stdout, fset, node, ast.NotNilFilter)
复制代码


  • traversal 方法:Inspect 方法和 Walk 方法用于遍历树,这两种方法都使用深度优先遍历访问 AST 树。不过,Walk 方法默认调用节点的 Visit 方法,而在 Inspect 方法中,可以通过定义函数来添加额外逻辑,后者在实现检查器时应用得更多。

定义 Linter

Mastering Go: In-Depth Analysis of Uber and Google’s Coding Standards 中,我们知道 Uber 建议用 var 代替 {} 进行切片初始化,下面将定义一个简单的 Linter 来进行检查。


  • 定义分析器


var InitChecker = &analysis.Analyzer{ Name: "InitChecker", Doc:  `This analyzer suggests "good" initilization behaviors.`, Run:  runAnalyzer, Requires: []*analysis.Analyzer{  inspect.Analyzer, },}
复制代码


  • 初始化 ast 树


inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{  (*ast.AssignStmt)(nil),  (*ast.GenDecl)(nil),  (*ast.CompositeLit)(nil), }
复制代码


  • PreOrder 中检查切片的初始化声明


inspect.Preorder(nodeFilter, func(n ast.Node) {  switch x := n.(type) {  case *ast.AssignStmt:   // check for slice init `var a []int` should be `a := []int{}`   // check for map init with make   for _, rhs := range x.Rhs {    if cl, ok := rhs.(*ast.CompositeLit); ok && cl.Type != nil && len(cl.Elts) == 0 {     switch cl.Type.(type) {     case *ast.ArrayType:      pass.Reportf(cl.Pos(), "consider using 'var' for empty slice initialization to avoid unnecessary memory allocation")    }   }  } })
复制代码


如果需要扩展这个检查器,比如检查 make 是否用于 map 的初始化,只需在上面的 switch 中添加以下内容即可。


     case *ast.MapType:      pass.Reportf(cl.Pos(), "consider using 'make' for map initialization to be explicit about intent")     }
复制代码
自定义 Golangci 检查器

golangci-lint 是一个集成了多种检查器的工具,支持通过配置文件定制和启用不同的检查器。要在 golangci-lint 中使用自定义检查器,需要将它们编译为插件,然后在 .golangci.yml 配置文件中指定插件路径。例如:


linters-settings:  custom:    initcheck:      path: ./path/to/initcheck      description: Checks if initializations follow Uber's style guide      original-url: "https://github.com/xxx"
复制代码

结论

本文通过对 Go analysis 软件包的探索,分享了构建分析器的见解,这些分析器是改进代码质量的基础。通过对软件包的主要 API(尤其是 PassFact)的了解,介绍了它们在构建强大的上下文感知工具中的重要作用。通过实际例子,进一步深入了解了 ast 软件包的复杂性,解读了 Node 概念及其变体,如表达式和语句。有了这些基础,我们才能设计出旨在优化切片初始化的检查器。最后,详细介绍了与 golangci-lint 的集成过程,通过简洁的方法,将自定义检查整合到 Go 项目中。这项工作不仅提高了代码质量,还增强了我们对 Go 分析能力的理解。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
5 分钟搞定 Golang 自定义代码分析器_golang_俞凡_InfoQ写作社区