闭包捕获的是变量地址而非值拷贝,所有闭包共享同一内存地址,导致修改相互影响;例如for循环中若闭包引用循环变量i,最终所有闭包都指向最后一次迭代的i值。

闭包捕获的是变量地址,不是值拷贝
Go 闭包对外部变量的引用是直接绑定内存地址,不是复制一份快照。这意味着所有闭包读写的都是同一块内存——哪怕变量是 int、string 这类值类型,只要它被多个闭包共享,修改就会相互影响。
- 现象:
for i := 0; i 最终调用全部输出 <code>3 - 原因:
i是循环变量,整个循环只分配一次栈空间,所有闭包捕获的都是&i - 验证方式:在循环里加
log.Printf("i addr: %p", &i),会发现地址始终不变
for 循环中创建闭包必须隔离变量
循环体内不隔离,闭包就必然复用同一变量。没有“默认安全”的写法,必须显式切断引用链。
- 推荐写法:
v := v; go func() { process(v) }()—— 每次迭代新建局部变量,地址不同,闭包捕获独立实例 - 等效写法:
go func(val string) { process(val) }(v)—— 参数传值,v在go语句执行时求值并复制,闭包内val是独立参数 - 禁止写法:
go func() { process(v) }()—— 直接捕获循环变量,v可能在 goroutine 启动前就被覆盖或栈复用
defer + 闭包的参数求值时机容易混淆
defer 语句中的闭包,参数求值和函数体执行是两个时间点:参数在 defer 语句执行时立即求值,函数体在函数返回前才执行。
- 陷阱示例:
for i := 0; i 输出三个 <code>3 - 正确做法:
defer func(val int) { fmt.Println(val) }(i)——i在defer执行时取值,传入闭包参数 - 注意:
defer func(i int) { ... }(i)和go func(i int) { ... }(i)行为一致,都是立即求值传参
闭包持有大对象会阻碍 GC 回收
闭包隐式持有所捕获变量的全部引用。如果闭包被长期持有(比如注册为 HTTP handler、存入全局 map、启动后台 goroutine),而它又无意中捕获了大对象(如 *http.Request、几 MB 的 []byte),那这些内存就无法被 GC 回收。
- 高风险场景:
http.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) { ... })——r被闭包捕获,但 handler 执行完后仍可能被其他逻辑间接引用 - 安全做法:只传必要字段,例如
id := r.URL.Query().Get("id"); http.HandleFunc(..., func(...) { use(id) }) - 切片和 map 更危险:它们本身是头结构(指针+长度+容量),复制变量不等于复制底层数组/哈希表,仍共享数据
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











