1152 字
约 3 分钟
5
S09-B07 · 函数、闭包与"循环变量捕获"
2024-12-31
2026-11-07

适用读者:写 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. 自检

  1. 为什么 B 写法打印 3 3 3?
  2. Go 1.22 改了什么?由什么决定?
  3. 显式拷贝为什么能修好?
  4. func 类型实现接口的实际用途是什么?
  5. 命名返回值 + defer 会发生什么?

7. 下一篇

S09-X01 · 学习路线与待补充清单

基础层到此结束。下一篇给 Go 的学习路线与"旧素材缺口登记"(并发部分不在旧卡素材内)。


本篇属于系列《S09 · Go 语言》。旧文来源:A-1465–A-1470;扩展点 E-S09-008/009。三种闭包写法为 Go 1.22.2 实测。

S09-B07 · 函数、闭包与"循环变量捕获"
https://www.clxhxhhr.top/posts/5305/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。