编程 staticcheck 2026.2 的 SA9010:defer f() 丢掉了那个该在退出时跑的闭包

2026-11-08 00:04:30

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 清理。

复制全文 生成海报 SA9010 staticcheck Go lint defer

推荐文章

程序员茄子在线接单