staticcheck 2026.2 的 SA9010:defer f() 丢掉了那个该在退出时跑的闭包
SA9010 是 staticcheck 2026.2 引入的新检查,标题为「Returned function should be called in defer」,属于 SA9 分类。SA9 的定义是「high probability of being wrong」的可疑代码构造——误报率高于其他 SA 系列,但触发点通常也指向本该重构的地方。
最小复现
官方文档给的例子就是全部:
func f() func() {
// Do something.
return func() {
// Do something.
}
}
func g() {
defer f() // 报:deferred return function not called
}
f 返回一个闭包,作者的本意几乎总是「退出时执行这个闭包」,但 defer f() 推迟的是 f 本身,闭包被当作返回值丢弃。
判定只有一句类型判断
源码在 staticcheck/sa9010/sa9010.go,核心逻辑短到可以整段贴出来:
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9010",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Returned function should be called in defer`,
Since: "2026.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
func run(pass *analysis.Pass) (any, error) {
fn := func(n ast.Node) {
def := n.(*ast.DeferStmt)
if _, ok := pass.TypesInfo.TypeOf(def.Call).Underlying().(*types.Signature); ok {
report.Report(pass, def, "deferred return function not called")
}
}
code.Preorder(pass, fn, (*ast.DeferStmt)(nil))
return nil, nil
}
遍历所有 DeferStmt,取 defer 里那个 CallExpr 的静态类型,underlying 是 *types.Signature 就报。它不关心闭包会不会被调用、有没有参数、是不是 nil。
defer 的两阶段求值
Go 规范里 defer 语句执行时,被推迟调用的函数值和实参立即求值并保存,真正的调用留到外层函数返回前。两种写法的差别就落在这条规则上:
defer f() // 立即求值:函数值 f,无实参
// 退出时调用:f(),返回的闭包被丢弃
defer f()() // 立即求值:先跑 f() 得到闭包,闭包作为函数值保存
// 退出时调用:闭包()
defer f() 的清理动作从头到尾没有发生过:f 的副作用在函数退出前才执行,而它返回的闭包没人接。defer f()() 把 f() 提到 defer 那一行,闭包成为被推迟调用的对象,退出时才真正跑清理。
修复前后对照
// 修复前
defer f()
// 修复后
defer f()()
返回的闭包带参数时,加一对括号并不能调用,得先把返回值取出来:
func cleanup() func(int)
// 修复前(报 SA9010)
defer cleanup()
// 修复后
done := cleanup()
defer done(0)
边界:哪些报,哪些不报
按 *types.Signature 这一条判据推导:
| 写法 | 结果 | 原因 |
|---|---|---|
func f() func() + defer f() | 报 | 类型是函数签名 |
defer f()() | 不报 | 外层 CallExpr 的类型是返回函数的返回值,通常无类型 |
defer resp.Body.Close() | 不报 | Close() 返回 error,不是 Signature |
defer mu.Unlock() | 不报 | 无返回值 |
func cleanup() func(int) + defer cleanup() | 报 | 带参数的返回函数也照报,加括号同样不能直接调用 |
defer s.Stop()(Stop 返回 func()) | 报 | 方法返回值是闭包,同样命中 |
旧版本静默通过
SA9010 标记 Since: 2026.2,2025.1.x 这类更早的 staticcheck 里根本没有这个分析器。维护者 dominikh 在 PR #1546 的讨论里说明过:staticcheck 有意不对未知检查名报错,一是前后兼容,二是兼容自定义构建。
于是 CI 里这么写:
staticcheck -checks=SA9010 ./...
用旧版本跑,既不报错也不检查,流水线一路绿灯。同一个 PR 里还提到 -checks 的匹配区分大小写,写成 sa9010 不会生效——该 PR 想改成不敏感,被否决,只保留一种拼写。走 golangci-lint 的话,settings.staticcheck.checks 同样受内置 staticcheck 版本约束。
搜索陷阱:sa9010 与 os.Exit
GitHub 仓库里的 staticcheck/sa9010 目录并不只装过这一个检查。2024 年的 issue #1526(Detect defer calls skipped by os.Exit)有过社区补丁,把「defer 之后调用 os.Exit / log.Fatal」的实验性检查放进过这个包。那不是最终发布的 SA9010 语义,最终版本是 returned function 检查。搜「sa9010 os.Exit」会撞上那批无关代码。issue #1526 至今仍是 open,dominikh 的判断是没法断言这一定是错误——defer 也可能只用于 panic 清理。