golang.org/x/tools/go/analysis 这个包的文档,过去几年一直在描述同一件事:怎么写一个能被多个工具复用的静态分析器。它不是新检查器,也没有版本号或发布日期,更像是 Go 团队沉淀下来的一份接口规范。
核心判断很直接:go/analysis 不等于 go vet。vet 只是这套接口的其中一个驱动程序,跟 IDE、CI、Bazel 构建系统是平级关系。这个区分容易被忽略,但决定了你该把这份文档当成什么——不是一个工具的说明书,而是一整类工具该怎么互通的规则。
Analyzer 和 Pass:谁写逻辑,谁管调度
两个类型分工很清楚。Analyzer 描述"查什么",包含名字、文档、依赖列表(Requires)和一个 Run 函数。Pass 描述"在哪个包上查",由驱动程序在遍历包依赖时构造出来,再传给 Analyzer.Run 使用。
需要纠正一个常见误解:检查器作者不构造 Pass,Pass 是驱动程序造好、喂给 Run 函数的。作者要写的只是 Run 里的逻辑——拿到语法树和类型信息,找问题,调用 pass.Report 上报。
一个 Analyzer 的最小形状大致是这样:
var Analyzer = &analysis.Analyzer{
Name: "printfwrapper",
Doc: "检查是否为 printf 风格包装函数",
Requires: []*analysis.Analyzer{inspect.Analyzer},
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
// 用 pass.Files / pass.TypesInfo 分析,pass.Report 上报诊断
return nil, nil
}
作者省下的是命令行解析、文件遍历、跨包调度这些活。独立写一个检查器,这些事都得自己处理;接进 go/analysis,这些事全部丢给驱动程序。
不同驱动程序的分工也不一样:
| 驱动程序 | 典型场景 | 是否分析标准库 | 诊断优先级谁定 |
|---|---|---|---|
| go vet | 本地 / CI 基础检查 | 是 | 驱动内置规则 |
| Bazel / Blaze 驱动 | 大型仓库构建时检查 | 部分不分析 | 平台团队配置 |
| 编辑器 / IDE 集成 | 编辑时实时提示 | 视实现而定 | 编辑器设置 |
| singlechecker / multichecker | 打包成独立 CLI 命令 | 视调用方式而定 | 命令行参数 |
也就是说,同一个 Analyzer 换个驱动,行为可能完全不一样——这不是 bug,是这套接口的设计前提。
Facts 能跨包复用,但不能假设标准库一定有
Facts 是让"模块化"落地的机制。一个分析器检查完低层包,可以留下一条事实,比如"某函数是 printf 包装器";检查调用它的高层包时直接读,不用重新推导。这跟分离编译的思路类似:只处理变化的部分。
要说清楚它的边界:Facts 只在同一次驱动运行内跨包传递,是否跨多次运行做缓存、怎么调度依赖顺序,这些都是驱动程序自己的事,框架本身不承诺增量执行的性能收益。
现实里还有个具体限制:部分基于 Bazel、Blaze 的驱动不分析标准库包,这些驱动下标准库的 Facts 不存在。printf 检查器本可以从 log.Printf 的源码推导出它是包装函数,但为了在这类驱动下也能正常工作,作者选择把这条结论直接硬编码进分析器,不依赖运行时推导。
分析器作者要记住这一条:不能假设标准库的 Facts 永远可用。跨驱动移植时,这种隐藏依赖最容易被漏掉,写单元测试时至少覆盖一次"无标准库 Facts"的场景。
另一个容易被忽略的设计:Diagnostic 结构体没有严重级别字段。框架的态度是——诊断该不该报、报出来算几级,是驱动程序和用户的事,分析器不该管。同一个 Analyzer 换个驱动,弹出来的优先级可能完全不同。
对谁有影响,该做什么
已经在写检查器的团队,迁移成本主要是把现有逻辑套进 Analyzer + Run 的形状,声明好 Requires。做完这一步,可以直接用 singlechecker 或 multichecker 打包成独立命令,不用再自己写命令行解析和文件遍历。
负责 IDE、CI 或大型仓库质量平台的团队,省下的是接入成本——同一批 Analyzer 可以喂给不同驱动。但规则清单开哪些、诊断按什么优先级弹出来、要不要屏蔽某些包,这些治理工作接口不会替你做,还是得平台团队自己定。
接下来值得盯的变量是:更多主流 IDE 和 CI 厂商会不会把 go/analysis 当成标准接入方式,而不是各自维护一层定制适配;以及 Bazel/Blaze 这类驱动会不会逐步补上标准库分析的缺口。这两件事没解决之前,"一套 Analyzer 到处能用"更多是理论上的可能性,不是当下的默认体验。
【锐评】工欲善其事,先利其器;器已备好,治理仍要驱动程序亲力亲为。
