defer捕获的是变量地址而非值,循环中所有defer共享同一内存地址,执行时读取的是终值;正确做法是用i := i创建局部副本或显式传参。

defer 中变量捕获的常见误解
Go 的 defer 不是“记录变量值”,而是“记录变量的地址或引用”。如果在循环或条件分支中声明变量再 defer,容易误以为每次 defer 都捕获了当时的值,实际可能全指向同一个变量内存位置。
为什么 for 循环里直接 defer i 会打印多个相同数字
因为 i 是循环变量,整个循环共用一个 i 实例。所有 defer 语句都闭包捕获的是这个变量的地址,等真正执行时 i 已经变成终值(比如 3)。
常见错误写法:
for i := 0; i < 3; i++ {
defer fmt.Println(i) // 输出:3 3 3
}
解决方式只有两种:
立即学习“go语言免费学习笔记(深入)”;
- 用局部变量显式拷贝:在循环体内用
:=声明新变量,让每次 defer 捕获独立副本 - 把 defer 放进立即执行的匿名函数中,通过参数传值(本质也是做一次值拷贝)
推荐写法:
for i := 0; i < 3; i++ {
i := i // 关键:重新声明同名变量,创建新绑定
defer fmt.Println(i) // 输出:2 1 0
}
defer 调用函数时参数求值时机很关键
defer 语句本身执行时,就立刻对函数参数求值(不是 defer 执行时),这是决定捕获内容的核心规则。
例如:
x := 1 defer fmt.Println(x) // 此刻 x=1,参数已确定为 1 x = 2 // 最终输出 1,不是 2
但若参数是函数调用,则调用发生在 defer 语句执行时:
func getValue() int { return x }
x := 1
defer fmt.Println(getValue()) // 此刻 getValue() 被调用,返回 1
x = 2
// 仍输出 1
而如果是 defer 一个闭包,情况不同:
x := 1
defer func() { fmt.Println(x) }() // x 是自由变量,延迟到执行时读取
x = 2
// 输出 2
结构体字段、指针、切片的捕获行为差异
捕获的是「表达式求值结果」,不是「变量本身」。这导致不同数据类型的 defer 行为差异明显:
- 基础类型(
int,string):参数求值即拷贝值,安全 - 指针(
*T):求值结果是地址,defer 执行时读的是该地址当前内容 - 切片(
[]T):求值结果是底层数组指针+长度+容量,若原切片后续被append导致扩容,defer 里看到的仍是旧底层数组(可能已被覆盖) - 结构体:按字段逐个求值;若含指针字段,该字段仍指向运行时最新地址
所以不要假设 defer 能“冻结”复杂数据状态——它只冻结参数表达式的求值结果,不冻结运行时内存内容。
最易被忽略的是:哪怕用了 i := i 这种技巧,如果 defer 的是 func() { ... } 且里面访问的是外部变量(非参数),依然会读到最终值。捕获逻辑只作用于 defer 语句那一刻的参数和变量绑定,不递归冻结闭包环境。


















