Go 的 staticcheck 抑制:配置文件、行级指令与增量清理顺序
1. 问题形状:只告知不阻塞,最后没人看
neugierig.org 的《Rethinking errors, warnings, and lints》给了一个基本框架:检查只有两种处理方式——inform(告知后继续)或 block(阻塞)。只告知不阻塞,长期结果是 warning blindness:检查报的问题太多,重要信号淹没在已被忽略的噪音里,检查最终不再起作用。
可以某个阶段只告知,但必须有一个更晚的阶段对同一件事阻塞,否则问题会一直留着。
C 社区把 -Werror 当作唯一可用的强制手段,把 warning 全变成 error,靠阻塞编译逼着修复。Gentoo devmanual 同时提醒 -Werror 不宜进 release 构建:新版本 GCC/glibc 会冒出新的告警、autoconf 检查会失败、老架构的告警也不同。它建议用 -Wno-error=具体告警 建容错区,而不是 -w 全关。
2. Go 里 warnings 从哪来
Go 编译器几乎没有 warning 这一级:未使用的 import、未使用的变量直接是编译错误,构建失败。所以 Go 没有 -Werror 这种开关,阻塞 = 进程非零退出。
逻辑类问题交给 go vet(标准库自带)和 staticcheck。编译器放行的东西,vet 能拦住:
fmt.Printf("Hello, %d\n", name) // name 是 string
输出:fmt.Printf format %d has arg name of type string。fmt 接受 any,编译器不报,go vet 报。
staticcheck 的安装与驱动方式:
go install honnef.co/go/tools/cmd/staticcheck@latest
go vet -vettool=$(which staticcheck) ./...
用 go vet 作为 driver,它会按 go build 的方式处理模块、构建约束、包路径;直接跑 staticcheck 可能漏包或踩 build tag。
SA1019 是「使用已弃用标识符」。staticcheck 记录了标准库每个符号的弃用版本,配合 -go 只报目标版本适用的弃用告警。
3. 三条抑制路径及各自代价
配置文件
文件名 staticcheck.conf,TOML 格式,按包子树生效;多层配置合并,深的覆盖浅的,未设置的项继承上层。特殊值 "inherit" 用于数组追加/删除。
默认配置(等价):
checks = [
"all",
"-SA9003",
"-ST1000",
"-ST1003",
"-ST1016",
"-ST1020",
"-ST1021",
"-ST1022",
"-ST1023",
]
"all" 匹配全部,- 前缀排除。举例 checks = ["inherit", "ST1000"] 表示继承已启用集合并额外打开 ST1000。
CLI flags 用于单次调用场景(build tags、输出格式),配置文件用于全项目一致设置。代价:配置排除是长期豁免,粒度到包/项目,代码行上不留痕迹,也看不出当初为什么放行。
行级指令
//lint:ignore Check1[,Check2,...,CheckN] reason
只影响下一行代码。reason 是必填字段,必须说明为什么这一行可以忽略,它是给后来人(包括未来的你)看的文档。示例:测试里故意比较两个 errors.New 结果是否相等,SA4000 会报「== 两边相同」,用 //lint:ignore SA4000 we want to make sure that no two results of errors.New are ever the same。
代价:逐行压制,指令本身成了要维护的代码,没匹配到问题就会被报出来。
文件级指令
//lint:file-ignore U1000 Ignore all unused code, it's generated
与行无关,惯例放文件顶部,代码生成产物常用。与行级不同,文件级指令不会因为「没匹配到问题」被判为多余。代价:整文件豁免,粒度最粗。
4. 指令维护的坑
staticcheck 会把「没用的指令」本身当成一个问题报出来:
tmp.go:1:2: this linter directive didn't match anything; should it be removed?
但通过配置文件禁用的检查,不会让对应指令被判为无用。配置排除是长期豁免,行级指令是临时豁免,两者语义不同。
想审计被指令压掉的告警,-show-ignored 会把本来会被指令忽略的问题输出出来,配合 JSON 格式可做统计。
行级指令必须紧贴目标行的上一行。写在别处(另一个文件、远处注释)就会造成 action at a distance:代码一挪,检查突然又亮,而且看不出是哪个旧位置引来的。
社区实际写法有差异:golangci-lint 用的是 //nolint:staticcheck // SA1019 reason;Kubernetes 曾把 //lint:ignore 加 //nolint 两行合并成一条 //nolint 带原因。两套语法别混着用。
5. 落地的增量顺序
- 先量基线:
staticcheck -f json ./...统计条数,按检查号分组,别按文件分组——按文件只会得出「这个目录最烂」这种没用的结论。 - CI 只对新增代码阻塞:新改动的包必须零告警;老包用配置或指令临时豁免。
- 每碰一个包就清一个包(boyscout rule),不新增问题,顺手修掉旧的。
- 抑制必须带 reason,reason 里写清「为什么现在不能修」和「什么时候能删」,否则半年后没人敢动这行。
- 顺序上先修 SA(逻辑/正确性)类,再修 S(简化)类,最后才是 ST(风格)类;风格类直接配置排除,不要用指令逐行压。