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 的最小形状大致是这样:

GO
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 只在同一次驱动运行内跨包传递,是否跨多次运行做缓存、怎么调度依赖顺序,这些都是驱动程序自己的事,框架本身不承诺增量执行的性能收益。

Driver 怎么串起 Pass 和 Facts Driver vet / IDE / CI / Bazel 构造 Pass 按依赖顺序逐包生成 调用 Run Analyzer.Run(pass) 下层包 Run ExportObjectFact 写入 Driver 暂存 Fact 本次运行内按包缓存 上层包 Pass ImportObjectFact 读取 Facts 只在同一次 Driver 运行内跨包传递,跨运行是否缓存由驱动自己决定

现实里还有个具体限制:部分基于 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 到处能用"更多是理论上的可能性,不是当下的默认体验。

【锐评】工欲善其事,先利其器;器已备好,治理仍要驱动程序亲力亲为。