适用读者:维护老 Go 项目、准备升级 Go 版本的人。
前置:S09-B07。
实测环境:Go 1.22.2(2026-11-05)。
覆盖旧文:—(E 驱动)
覆盖扩展:E-S09-009 深化(go.mod的 go 指令与语言语义)。
上一篇:S09-J02 · 字符串与 UTF-8:byte、rune 与 O(n) 陷阱
下一篇:S09-A01 · 模块与依赖。
1. 一句话结论
Go 1.22 改了 for 循环变量的作用域:每次迭代一个独立变量。这修掉了历史悠久的闭包 bug,但也可能改变依赖旧行为的代码。
2. 实测:三种写法的结果(Go 1.22.2)
// A) 循环内声明(1.22 新语义)
for i := 0; i < 3; i++ { fs = append(fs, func() int { return i }) }
→ 0 1 2
// B) 循环外声明(永远共享)
var j int
for j = 0; j < 3; j++ { fs = append(fs, func() int { return j }) }
→ 3 3 3
// C) 显式拷贝(任何版本安全)
for k := 0; k < 3; k++ { kk := k; fs = append(fs, func() int { return kk }) }
→ 0 1 2
| 写法 | 1.21 预期 | 1.22 实测 |
|---|---|---|
| A | 3 3 3 |
0 1 2 |
| B | 3 3 3 |
3 3 3 |
| C | 0 1 2 |
0 1 2 |
待核:本文尝试通过把
go.mod改为go 1.21来复现旧语义,未成功(A 仍输出0 1 2)。说明"语言版本 → 语义"的映射在本机工具链上未按预期生效。结论:升级前用本文 A/B/C 三段代码在目标工具链实测,不要依赖文档推断。
3. 为什么 B 永远危险
j 是循环外的变量,三个闭包共享它。循环结束时 j == 3,闭包在循环后执行 → 全读 3。
这不是 Go 的 bug,是"捕获变量"的正常语义。语言能做的只是"让 for 内声明的变量不再被共享"——而"循环外声明"是你主动选择的共享。
4. 升级检查清单
| # | 检查项 | 做法 |
|---|---|---|
| 1 | 找出所有"闭包 + 循环" | grep 闭包字面量出现在 for 内 |
| 2 | 区分"捕获循环变量"还是"捕获外部变量" | 看变量声明位置 |
| 3 | 依赖旧行为的地方 | 显式加 kk := k(语义不变,可读性更好) |
| 4 | 测试 | 跑全量测试;重点看并发/回调相关 |
| 5 | 工具链 | go.mod 的 go 指令 + CI 的 Go 版本要一致 |
判据:"这段代码在 1.21 与 1.22 下会得到不同结果吗?" 会 → 升级前显式改写,别赌语义。
5. 与 goroutine 的关联(前瞻,本系列不展开)
历史 bug 的典型形态:
for _, v := range items {
go func() { process(v) }() // 1.21: 全部用最后一个 v
}
在 1.22 下这个 bug 自动消失——这是升级的收益。但并发本身(调度、竞态、channel)不在本系列范围(旧素材缺口,见 X01 §2),细节归 S05。
6. 五个误区
| 误区 | 真相 |
|---|---|
| "Go 1.22 修好了所有闭包问题" | 只修 for 内声明;外部变量照样共享 |
"改 go.mod 就能回到旧语义" |
实测未复现;以工具链实测为准 |
| "显式拷贝是过时写法" | 仍是"表达意图最清楚"的写法,跨版本安全 |
| "升级只会带来好处" | 依赖旧行为的代码可能出错(少见但存在) |
| "闭包 bug 只出现在并发里" | 串行也会(B 写法就是串行 demo) |
7. 自检
- A/B/C 三种写法在 1.22.2 下分别输出什么?
- 为什么 B 无法通过语言升级修好?
- 升级前的最小验证方式是什么?
go.mod的go指令在本机实验中起效了吗?- 为什么说"显式拷贝跨版本安全"?
8. 下一篇
S09-A01 · 模块与依赖:go.mod、GOPROXY 与可复现构建
进阶层到此结束。下一篇进入架构师层:依赖从哪来、怎么保证可复现、拉不到包怎么排查。
本篇属于系列《S09 · Go 语言》。扩展点 E-S09-009 深化。A/B/C 输出为 Go 1.22.2 实测(旧语义未复现,已标待核)。