Go 分析框架:Go 团队开发的模块化静态分析
摘要
Go 分析框架为 Go 代码的静态分析提供模块化接口,支持可复用的检查器,可应用于 vet、IDE 和构建系统等多种工具。
暂无内容
查看缓存全文
缓存时间: 2026/07/27 01:42
# analysis 包 - golang.org/x/tools/go/analysis - Go Packages
来源:https://pkg.go.dev/golang.org/x/tools/go/analysis
包 analysis 定义了模块化静态分析与分析驱动程序之间的接口。
#### 背景¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Background)
静态分析是一个函数,它检查 Go 代码包并报告一组诊断(通常是代码中的错误),也可能产生其他结果,例如建议的重构或其他事实。报告错误的分析通常被称为“检查器”。例如,printf 检查器报告 fmt.Printf 格式字符串中的错误。
“模块化”分析是一种每次检查一个包的分析,但它可以保存来自低层级包的信息,并在检查高层级包时使用,类似于工具链中的单独编译。printf 检查器是模块化的:当它发现诸如 log.Fatalf 这样的函数委托给 fmt.Printf 时,它会记录这一事实,并检查对该函数的调用,包括从其他包发出的调用。
通过实现一个通用接口,来自不同来源的检查器可以很容易地被选择、集成和重用,涵盖广泛的驱动程序,包括命令行工具(如 vet)、文本编辑器和 IDE、构建和测试系统(如 go build、Bazel 或 Buck)、测试框架、代码审查工具、代码库索引器(如 SourceGraph)、文档查看器(如 godoc)、大型代码库的批处理管道等。
#### Analyzer¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Analyzer)
API 中的主要类型是Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer)。Analyzer 静态描述了一个分析函数:其名称、文档、标志、与其他分析器的关系,以及当然,其逻辑。
要定义分析,用户声明一个类型为 Analyzer 的(逻辑上常量)变量。以下是 go/analysis/passes/ 子目录中一个分析器的典型示例:
``
package unusedresult
var Analyzer = &analysis.Analyzer{
Name: "unusedresult",
Doc: "检查对某些函数的调用结果是否未被使用",
Run: run,
...
}
func run(pass *analysis.Pass) (interface{}, error) {
...
}
``
分析驱动程序是一个程序(如 vet),它运行一组分析并打印它们报告的诊断。驱动程序程序必须导入其所需的 Analyzer 列表。通常每个 Analyzer 位于单独的包中。要向现有驱动程序添加新的 Analyzer,只需在列表中添加另一个项:
``
import ( "unusedresult"; "nilness"; "printf" )
var analyses = []*analysis.Analyzer{
unusedresult.Analyzer,
nilness.Analyzer,
printf.Analyzer,
}
``
驱动程序可以使用名称、标志和文档来提供描述其执行的分析的在线帮助。文档注释包含简短的摘要行,后面可选地跟有解释段落。
除了上面展示的字段,Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer) 类型还有更多字段:
``
type Analyzer struct {
Name string
Doc string
Flags flag.FlagSet
Run func(*Pass) (interface{}, error)
RunDespiteErrors bool
ResultType reflect.Type
Requires []*Analyzer
FactTypes []Fact
}
``
Flags 字段声明了一组命名的(全局)标志变量,用于控制分析行为。与 vet 不同,分析标志不直接在命令行 FlagSet 中声明;由驱动程序负责设置标志变量。对于单个分析 a,其驱动程序可能会在命令行上直接将其标志 f 公开为 -f,而对于多个分析,驱动程序可能会在标志名称前加上分析名称(-a.f)以避免歧义。IDE 可能通过图形界面公开标志,而批处理管道可能通过配置文件配置它们。请参见“findcall”分析器以了解标志的使用示例。
RunDespiteErrors 标志指示分析是否能够处理类型错误的代码。如果不能,如果存在解析或类型错误,驱动程序将跳过分析。可选的 ResultType 字段指定此分析计算并可供其他分析使用的结果值的类型。Requires 字段指定此分析依赖的分析列表,以及它可能访问的结果,并约束驱动程序运行分析的顺序。FactTypes 字段在模块化部分讨论。analysis 包提供了 Validate 函数,用于对 Analyzer 执行基本健全性检查,例如确保其 Requires 图是无环的,其事实和结果类型是唯一的等。
最后,Run 字段包含一个函数,驱动程序调用它在单个包上执行分析。驱动程序将 Pass 类型的一个实例传递给它。
#### Pass¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Pass)
Pass (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass) 描述单个工作单元:将特定的 Analyzer 应用于特定的 Go 代码包。Pass 向 Analyzer 的 Run 函数提供有关正在分析的包的信息,并向 Run 函数提供用于报告诊断和其他信息返回给驱动程序的操作。
``
type Pass struct {
Fset *token.FileSet
Files []*ast.File
OtherFiles []string
IgnoredFiles []string
Pkg *types.Package
TypesInfo *types.Info
ResultOf map[*Analyzer]interface{}
Report func(Diagnostic)
...
}
``
Fset、Files、Pkg 和 TypesInfo 字段为单个 Go 代码包提供语法树、类型信息和源代码位置。
OtherFiles 字段提供非 Go 文件(如作为此包一部分的汇编文件)的名称。类似地,IgnoredFiles 字段提供当前构建配置下不属于此包但可能属于其他构建配置的 Go 和非 Go 源文件的名称。可以使用 Pass.ReadFile 读取这些文件的内容;有关加载非 Go 文件并根据它们报告诊断的示例,请参见“asmdecl”或“buildtags”分析器。
ResultOf 字段提供由此分析器所需的分析器计算的结果,这些需求在其 Analyzer.Requires 字段中表达。驱动程序首先运行所需的分析器,并将它们的结果在此映射中提供。每个 Analyzer 必须返回其 Analyzer.ResultType 字段中描述的类型值。例如,“ctrlflow”分析器返回 *ctrlflow.CFGs,它为包中的每个函数提供控制流图(参见 golang.org/x/tools/go/cfg);“inspect”分析器返回一个值,使其他 Analyzer 能够更高效地遍历包的语法树;“buildssa”分析器构建 SSA 形式的中间表示。每个这些 Analyzer 扩展了后续 Analyzer 的能力,而无需向核心 API 添加依赖,因此分析工具只为其需要的扩展付出代价。
Report 函数发出一个诊断,一个与源位置关联的消息。对于大多数分析,诊断是它们的主要结果。为了方便,Pass 提供了一个辅助方法 Reportf,通过格式化字符串来报告新的诊断。Diagnostic 定义为:
``
type Diagnostic struct {
Pos token.Pos
Category string // 可选
Message string
}
``
可选的 Category 字段是一个短标识符,用于在分析产生多种诊断时对消息类型进行分类。
Diagnostic (https://pkg.go.dev/golang.org/x/tools/go/analysis#Diagnostic) 结构没有指示严重性的字段,因为用户对分析器及其诊断的相对重要性的看法差异很大。该框架的设计不要求每个 Analyzer 负责识别其诊断的严重性。相反,我们期望驱动程序允许用户根据产生诊断的 Analyzer 和可选的 Category,根据用户偏好来定制诊断的过滤和优先级排序。
大多数 Analyzer 检查带类型的 Go 语法树,但少数(如 asmdecl 和 buildtag)检查 Go 源文件的原始文本,甚至非 Go 文件(如汇编)。要根据原始文本文件的行报告诊断,请使用以下序列:
``
content, err := pass.ReadFile(filename)
if err != nil { ... }
tf := fset.AddFile(filename, -1, len(content))
tf.SetLinesForContent(content)
...
pass.Reportf(tf.LineStart(line), "oops")
``
#### 使用事实的模块化分析¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Modular_analysis_with_Facts)
为了提高效率和可伸缩性,大型程序通常使用单独编译构建:程序的单元被单独编译,并且仅在其依赖项之一发生变化时才重新编译;独立的模块可以并行编译。同样的技术可以应用于静态分析,以获得相同的好处。这样的分析被称为“模块化”。
编译器的类型检查器是模块化静态分析的一个例子。我们希望应用于 Go 程序的许多其他检查器可以被理解为替代的或非标准的类型系统。例如,vet 的 printf 检查器推断函数是否具有“printf 包装器”类型,并对这类函数的调用应用更严格的检查。此外,它记录哪些函数是 printf 包装器,供后续分析阶段通过归纳识别其他 printf 包装器使用。一个本身不有趣但作为达到有趣结果(如诊断)的垫脚石的结果(如“f 是一个 printf 包装器”)被称为事实 (https://pkg.go.dev/golang.org/x/tools/go/analysis#Fact)。
analysis API 允许分析定义新类型的事实,将这些类型的事实与当前包中声明的对象(命名实体)关联,或与包整体关联,并查询与对象或包关联的给定类型的现有事实。
使用事实的 Analyzer 必须声明其类型:
``
var Analyzer = &analysis.Analyzer{
Name: "printf",
FactTypes: []analysis.Fact{new(isWrapper)},
...
}
type isWrapper struct{} // => *types.Func f “是一个 printf 包装器”
``
驱动程序程序确保在分析包之前生成针对该包依赖项的事实,并负责将事实从一个包传播到另一个包,可能跨越地址空间。因此,事实必须可序列化。API 要求驱动程序使用 gob 编码,这是一种高效、健壮、自描述的二进制协议。如果默认编码不合适,事实类型可以实现 GobEncoder/GobDecoder 接口。事实应该是无状态的。由于序列化的事实可能出现在构建输出中,事实的 gob 编码必须是确定性的,以避免在使用内容寻址缓存的构建系统中出现虚假缓存未命中。驱动程序对给定分析阶段导出的所有事实进行一次 gob 编码调用,以便保留多个事实引用的共享数据结构的拓扑结构。
Pass 类型具有导入和导出事实的函数,事实可以与对象或包关联:
``
type Pass struct {
...
ExportObjectFact func(types.Object, Fact)
ImportObjectFact func(types.Object, Fact) bool
ExportPackageFact func(fact Fact)
ImportPackageFact func(*types.Package, Fact) bool
}
``
Analyzer 只能导出与当前包或其对象关联的事实,但它可以导入来自作为当前包导入依赖的任何包或对象的事实。
从概念上讲,ExportObjectFact(obj, fact) 将事实插入到以 (obj, TypeOf(fact)) 对为键的隐藏映射中,而 ImportObjectFact 函数从该映射检索条目并将其值复制到 fact 指向的变量中。此方案假设事实的具体类型是一个指针;此假设由 Validate 函数检查。请参见“printf”分析器以了解对象事实的使用示例。
一些驱动程序实现(例如基于 Bazel 和 Blaze 的实现)目前不将分析器应用于标准库的包。因此,为了获得最佳结果,分析器作者不应依赖于标准包的分析事实可用。例如,尽管 printf 检查器能够在分析 log 包时推断出 log.Printf 是一个 printf 包装器,但此事实内置于分析器中,以便即使在不对标准包应用分析器的驱动程序中运行时,也能正确检查对 log.Printf 的调用。我们希望在将来消除此限制。
#### 测试 Analyzer¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Testing_an_Analyzer)
analysistest 子包提供了用于测试 Analyzer 的实用工具。用几行代码,就可以在测试数据文件包上运行分析器,并检查它是否报告了所有预期的诊断和事实(且没有更多)。期望使用输入代码中的 “// want ...” 注释表达。
#### 独立命令¶ (https://pkg.go.dev/golang.org/x/tools/go/analysis#hdr-Standalone_commands)
分析器以包的形式提供,驱动程序程序应导入这些包。vet 命令导入一组几个分析器,但用户可能希望定义自己的执行额外检查的分析命令。为了简化创建分析命令的任务(无论是用于单个分析器还是整套分析器),我们提供了 singlechecker 和 multichecker 子包。
singlechecker 包为运行一个分析器的命令提供了 main 函数。按照惯例,每个分析器(例如 go/analysis/passes/findcall)都应附带一个基于 singlechecker 的命令,例如 go/analysis/passes/findcall/cmd/findcall,其完整定义如下:
``
package main
import (
"golang.org/x/tools/go/analysis/passes/findcall"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() { singlechecker.Main(findcall.Analyzer) }
``
提供多个分析器的工具可以以类似的方式使用 multichecker,向其传递 Analyzer 列表。
- [func Validate(analyzers [\]*Analyzer) error](https://pkg.go.dev/golang.org/x/tools/go/analysis#Validate)
- type Analyzer (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer)
- - func (a *Analyzer) String() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#Analyzer.String)
- type CycleInRequiresGraphError (https://pkg.go.dev/golang.org/x/tools/go/analysis#CycleInRequiresGraphError)
- - func (e *CycleInRequiresGraphError) Error() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#CycleInRequiresGraphError.Error)
- type Diagnostic (https://pkg.go.dev/golang.org/x/tools/go/analysis#Diagnostic)
- type Fact (https://pkg.go.dev/golang.org/x/tools/go/analysis#Fact)
- type Module (https://pkg.go.dev/golang.org/x/tools/go/analysis#Module)
- type ModuleError (https://pkg.go.dev/golang.org/x/tools/go/analysis#ModuleError)
- type ObjectFact (https://pkg.go.dev/golang.org/x/tools/go/analysis#ObjectFact)
- type PackageFact (https://pkg.go.dev/golang.org/x/tools/go/analysis#PackageFact)
- type Pass (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass)
- - func (pass *Pass) ReportRangef(rng Range, format string, args ...any) (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.ReportRangef) - func (pass *Pass) Reportf(pos token.Pos, format string, args ...any) (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.Reportf) - func (pass *Pass) String() string (https://pkg.go.dev/golang.org/x/tools/go/analysis#Pass.String)
- type Range (https://pkg.go.dev/golang.org/x/tools/go/analys
相似文章
当AI代码生成工具真正了解你的代码库时,Go语言中的AI代码生成效果会显著提升
本文认为,当AI代码生成工具了解组织内部的代码库和约定时,Go语言的代码生成会更有效,从而提高接受率并减少所需编辑次数。
Golang 代码审查笔记 II
来自 elttam 的后续博客文章,介绍了提高安全性的新 Go 语言特性、在代码审计中发现的有问题的编码模式(footguns),以及用于捕获这些模式的 Semgrep 规则。
Go 工具链的 Fuzzing 分支
Trail of Bits 推出了 gosentry,这是一个面向模糊测试的 Go 工具链分支,集成 LibAFL 以增强路径约束求解、结构化模糊测试及漏洞检测能力,同时保留标准测试工作流程。
watgo - 一个针对Go语言的WebAssembly工具包
watgo是一个纯Go语言的WebAssembly工具包,可以解析WAT、验证、编码为WASM二进制,以及解码二进制格式,同时提供CLI和Go API。
JetBrains Go现代编码规范指南
JetBrains发布了一个GitHub仓库,其中包含编写现代Go代码的代码代理指南,涵盖了Go 1.27的所有功能,并与Junie和Claude Code等工具集成。