在 Go 的世界里,defer 就像你雇的一个超靠谱管家:
你只管往前冲,他默默在你出门前关灯、锁门、浇花、顺手把猫粮倒上......
但如果你没交代清楚,他可能会在你出差时把房子点了🔥,还一脸无辜地说:"您没说不能烧啊。"
今天,我们就用几个短小精悍的例子,带你彻底搞懂 defer 的脾气------
哪些事他干得漂亮?哪些事千万别交给他?哪些"经典翻车"其实已经过时了?
一、defer 是啥?一句话说清
defer 把语句"记下来",等函数 return 前再执行。
go
func main() {
defer fmt.Println("我是 defer")
fmt.Println("我是正常代码")
}
// 输出:
// 我是正常代码
// 我是 defer
✅ 记住:不是 if 块结束,不是 for 循环结束,是整个函数 return 前!
二、曾经的"全民公敌":defer + 循环(现在好多了!)
🕰 老版本 Go(≤1.21):闭包陷阱,人人喊打
go
// Go ≤1.21
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
// 输出:2, 2, 2 ❌
为啥?因为所有 defer 共享同一个 i!等函数返回时,i 已经变成 3(循环结束),但最后一次有效值是 2,所以全打印 2。
这坑,坑哭了无数 Go 新手,连老鸟都得小心翼翼。
🌈 Go 1.22+:官方出手,修复语义! 从 Go 1.22 开始,每次循环迭代都会创建一个全新的 i!
go
// Go ≥1.22
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
// 输出:2, 1, 0 ✅
🎉 别慌!这不是 bug,这是 defer 在认真工作!
为什么是 2,1,0?因为 defer 是后进先出(LIFO)------
就像你往箱子里放盘子:先放 0,再放 1,最后放 2;拿出来时,2 最先被拿到!
💡 所以 2,1,0 = 循环变量已修复 + defer 顺序正确 = 双喜临门!
🛡 那还要"传参冻结"吗?
go
// Go 1.22+ 下,这已经安全了!
for i := 0; i < 3; i++ {
defer func() { fmt.Println(i) }()
}
但如果你:
要兼容老版本 Go 在 goroutine 里用(比如 go func(){...}()) 想让代码意图更清晰 那还是推荐"传参大法":
go
for i := 0; i < 3; i++ {
defer func(x int) { fmt.Println(x) }(i)
}
简单、安全、跨版本通吃,何乐不为?
三、依然危险的雷区:defer 别乱改返回值!
这个坑,Go 1.0 到 1.25 都存在,而且特别隐蔽:
go
func tricky() (x int) {
defer func() { x = 100 }() // 修改命名返回值
return 10
}
// 返回 100!
😱 为什么?因为 x 是"命名返回值",在函数开头就分配好了内存。
return 10 实际是 x = 10; return,而 defer 在 return 前执行,把 x 改成了 100。
但如果你不用命名返回值:
go
func safe() int {
x := 10
defer func() { x = 100 }() // 改的是局部变量副本
return x // 返回 10
}
✅ 建议:除非你在写"迷惑行为大赏",否则别在 defer 里动返回值!
四、结构体方法里的 defer:值接收者 vs 指针接收者
go
type Cat struct{ lives int }
// 值接收者 → 拿到的是猫的"照片"
func (c Cat) LoseLife() {
defer func() { c.lives-- }() // 照片上的猫死了,真猫没事
}
// 指针接收者 → 拿到的是猫本猫
func (c *Cat) LoseLife() {
defer func() { c.lives-- }() // 真猫少了一条命!
}
✅ 原则:想改结构体?用 *Cat,别用 Cat!
五、高频循环里用 defer?小心性能刺客!
go
// ❌ 千万别这么干!
for i := 0; i < 1000000; i++ {
f, _ := os.Open("log.txt")
defer f.Close() // 100 万个 defer 等函数结束才执行!
}
后果:
内存爆炸(defer 链表超长) 文件句柄耗尽(系统 limit 被打满) 同事提刀找你 ✅ 正确姿势:用匿名函数"圈地自闭"
go
for i := 0; i < 1000000; i++ {
func() {
f, _ := os.Open("log.txt")
defer f.Close() // 这个 defer 只活到匿名函数结束!
// do something
}()
}
安全、高效、不背锅!
六、记录函数耗时?别被"假 defer"骗了!
很多人这么写:
go
// ❌ 错!耗时永远≈0
start := time.Now()
defer log.Printf("took %v", time.Since(start))
为什么错?因为 time.Since(start) 在 defer 注册时就计算好了!
等函数跑完 10 秒,日志却显示 took 5µs......
✅ 正确写法:把计算塞进匿名函数!
go
start := time.Now()
defer func() {
log.Printf("took %v", time.Since(start)) // 返回时才算!
}()
这样,耗时才真实可信。
(不信?自己跑个 time.Sleep(2*time.Second) 试试!)
七、defer 的高光时刻(放心交给管家!)
关文件
go
f, _ := os.Open("a.txt")
defer f.Close()
解锁
go
mu.Lock()
defer mu.Unlock()
兜底 panic
go
defer func() {
if err := recover(); err != nil {
log.Println("稳住,我们能赢:", err)
}
}()
测耗时(用对姿势!)
go
start := time.Now()
defer func() { log.Printf("耗时:%v", time.Since(start)) }()
八、终极口诀:defer 使用三原则(Go 1.22+ 版)
🧘♂️ 背下来,保平安!
循环里用 defer?Go 1.22+ 安全,但输出是反的(LIFO)! 别在 defer 里改返回值------除非你想让同事怀疑人生! 高频场景用匿名函数包裹,别让 defer 成为性能刺客!
结语:defer 是好同志,但得"明明白白"地用
defer 不是魔法,它只是个守规矩的管家。
只要你交代清楚(用对写法),他就能帮你写出简洁、安全、优雅的 Go 代码。
但如果你含糊其辞(比如指望它自动测耗时),那他可能真的会把猫粮倒进鱼缸,还说:"您没说鱼不吃猫粮啊。"
📌 动手建议:复制文中的例子,go run 一下,亲眼看看输出。
编程的真理,不在文档里,在你的终端里!