go闭包捕获的是变量地址而非值快照,导致循环中多个闭包共享同一变量,输出全为终值;需用s:=s或带参立即执行来隔离变量;闭包适用于封装状态、延迟求值等,但需警惕内存泄漏、竞态及指针误用。

闭包在 Go 里到底捕获的是什么
Go 的闭包不是“值的快照”,而是对变量地址的引用。这意味着多个闭包可能共享同一个变量实例,修改一个会影响另一个——哪怕它们来自不同调用。
- 循环中
i被反复赋值,所有func() { fmt.Println(i) }都指向同一块内存,最终输出全是3 - 外层函数返回后,只要闭包还存活(比如被赋给全局变量、传进 goroutine、或仍在栈上),它捕获的变量就不会被 GC 回收
- 如果捕获的是大对象(如
[]byte或结构体),而闭包长期驻留,可能造成意外内存堆积
for 循环里创建闭包的两种可靠写法
这是 Go 新手最常翻车的地方。别指望编译器自动帮你“快照”循环变量;必须显式切断变量复用。
-
推荐写法:短变量声明 —— 在循环体内加一行
s := s,让每次迭代都生成独立绑定:for _, s := range []string{"a", "b", "c"} { s := s // ✅ 关键 funcs = append(funcs, func() { fmt.Println(s) }) } -
等效写法:带参立即执行 —— 把当前值作为参数传入匿名函数:
for _, s := range list { funcs = append(funcs, func(val string) { fmt.Println(val) }(s)) } - 不要用
new(func())或make(),Go 中函数类型不可分配内存,这类写法会编译失败
闭包适合干哪些事,又该避开哪些坑
闭包真正有用的地方,是封装状态、延迟求值、简化回调——但前提是别让它变成内存泄漏或竞态的源头。
- 计数器:
newCounter()返回的闭包能安全持有count,编译器自动将其提升到堆上 - 日志包装:
log := func(msg string) { fmt.Println("[API]", msg) },轻量且可携带上下文 - HTTP 中间件:
func(next http.Handler) http.Handler是典型闭包链,预处理逻辑靠捕获的变量维持 - ⚠️ goroutine + 闭包组合时尤其危险:
go func() { fmt.Println(i) }()会并发读取同一个i,结果不确定,极大概率全输出3
指针和闭包一起用要格外小心
闭包本身已经按引用捕获变量,再套一层指针容易多绕一重间接,反而增加理解成本和出错概率。
立即学习“go语言免费学习笔记(深入)”;
- 普通闭包已足够实现状态共享,比如
counter()就不需要*int - 若真需要跨多个闭包强同步某值,用指针可以,但得确保每个闭包操作的是同一指针(
p := &i),而不是误把&i写成&s后还在循环里复用 - 错误示范:
p := &i放在循环外,所有闭包都指向同一个i地址;正确做法是循环内先复制值,再取地址,或直接用值闭包
go 就自动修复。每次写 for + 闭包,都得下意识问一句:这个变量,我捕获的是它的现在,还是它的最后?










