本文介绍了如何利用 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
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
...
}
复制代码
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
// Ident
var x
// BasicLit
42
"hello"
// BinaryExpr
a + b
// CallExpr
fmt.Println("hello world!")
// IndexExpr
arr[1]
m["key"]
// SliceExpr
s[1:5]
// TypeAssertExpr
x.(int)
// UnaryExpr
&x
!b
// CompositeLit
[]int{1,2,3}
复制代码
处理的功能包括:
// 定义 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)
复制代码
定义 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,
},
}
复制代码
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
nodeFilter := []ast.Node{
(*ast.AssignStmt)(nil),
(*ast.GenDecl)(nil),
(*ast.CompositeLit)(nil),
}
复制代码
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(尤其是 Pass
和 Fact
)的了解,介绍了它们在构建强大的上下文感知工具中的重要作用。通过实际例子,进一步深入了解了 ast
软件包的复杂性,解读了 Node
概念及其变体,如表达式和语句。有了这些基础,我们才能设计出旨在优化切片初始化的检查器。最后,详细介绍了与 golangci-lint 的集成过程,通过简洁的方法,将自定义检查整合到 Go 项目中。这项工作不仅提高了代码质量,还增强了我们对 Go 分析能力的理解。
你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!
评论