适用读者:写 Go 闭包时结果"看起来对但输出全一样"的人。
前置:S09-B02。
实测环境:Go 1.22.2(2026-11-05)。
覆盖旧文:A-1465(21) A-1466(68) A-1467(11) A-1468(29) A-1469(27) A-1470(31) = 187 点。
覆盖扩展:E-S09-008(捕获变量 vs 捕获值)、E-S09-009(Go 1.22 loopvar 变更)。
上一篇:S09-B06 · 控制流:for是唯一的循环
下一篇:S09-X01 · 学习路线与待补充清单。
1. 一句话结论
闭包捕获的是"变量",不是"值的快照"。 所以如果多个闭包共享同一个变量,它们看到的是这个变量的最终值。
实测三种写法(Go 1.22.2):
| 写法 | 输出 | 说明 |
|---|---|---|
A for i := 0; i < 3; i++ { fs = append(fs, func() int { return i }) } |
0 1 2 |
Go 1.22 起每次迭代独立变量 |
B var j int; for j = 0; j < 3; j++ { ... return j } |
3 3 3 |
变量在循环外 → 共享,永远踩坑 |
C 显式拷贝 kk := k; ... return kk |
0 1 2 |
任何版本都安全 |
2. 为什么 B 会打印 3 3 3
for j = 0; j < 3; j++ {
fs = append(fs, func() int { return j })
}
- 三个闭包都引用同一个变量
j; - 循环结束时
j == 3; - 闭包在循环之后才被调用 → 全部读到 3。
这是"闭包捕获变量引用"的直接后果,与语言无关(Java 的 lambda 要求 effectively final,就是在语言层堵住这个坑)。
3. Go 1.22 的语义变更(E-S09-009)
| 版本 | for i := ... 里的 i |
|---|---|
| ≤ 1.21 | 循环内共用一个 i → 闭包全看到最终值 |
| ≥ 1.22 | 每次迭代创建新变量 → 闭包各自看到当次值 |
生效条件:由 go.mod 里的 go 指令决定(语言版本),不是由工具链版本决定。
待核说明:本机 Go 1.22.2 下把
go.mod改成go 1.21后,A 写法仍然输出0 1 2(新语义),旧语义未能复现。结论:实际行为以你的工具链为准,不要仅凭go.mod判断——升级后请用一段最小代码实测(本文给出的 A/B/C 三写法即可)。
这条对维护老项目很重要:升级 Go 版本可能悄悄改变闭包行为——既是修复,也可能是"依赖旧行为"的代码出错。
4. 函数的几种形态
4.1 多返回值与命名返回值
func divmod(a, b int) (q, r int) { return a / b, a % b }
命名返回值的坑:配合 defer 可以修改返回值(常用于错误包装),但也容易让人误判"return 之后就定了"。
4.2 函数是一等值(A-1467)
var f = func(x int) int { return x * 2 }
fmt.Println(f(3)) // 6
4.3 函数实现接口(A-1469)
type HandlerFunc func(string) error
func (h HandlerFunc) Serve(s string) error { return h(s) }
这是 Go 的"适配器"惯用法:http.HandlerFunc 就是标准库里的同款写法。
4.4 闭包做状态容器(A-1470)
func counter() func() int {
n := 0
return func() int { n++; return n }
}
c := counter()
c(); c() // 返回 1, 2
用途:把"状态 + 操作"封在一起,避免全局变量。
5. 五个误区
| 误区 | 真相 |
|---|---|
| "闭包捕获的是值" | 捕获的是变量(除非显式拷贝) |
| "Go 1.22 之后所有闭包都安全" | 只解决"for 内声明"的场景;循环外变量照样踩 |
"go.mod 声明决定一切" |
实测未复现旧语义,以工具链实测为准 |
| "命名返回值只是语法糖" | 会改变 defer 修改返回值的语义 |
| "函数实现接口很怪" | 这是标准库惯用法(http.HandlerFunc) |
6. 自检
- 为什么 B 写法打印
3 3 3? - Go 1.22 改了什么?由什么决定?
- 显式拷贝为什么能修好?
func类型实现接口的实际用途是什么?- 命名返回值 +
defer会发生什么?
7. 下一篇
S09-X01 · 学习路线与待补充清单
基础层到此结束。下一篇给 Go 的学习路线与"旧素材缺口登记"(并发部分不在旧卡素材内)。
本篇属于系列《S09 · Go 语言》。旧文来源:A-1465–A-1470;扩展点 E-S09-008/009。三种闭包写法为 Go 1.22.2 实测。