一文讲透 Go 的 defer:你的“善后管家“,别让他变成“背锅侠“!

在 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 一下,亲眼看看输出。

编程的真理,不在文档里,在你的终端里!

相关推荐
Mgx3 小时前
剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
go
百锦再10 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
百锦再14 小时前
第12章 测试编写
android·java·开发语言·python·rust·go·erlang
Mgx2 天前
你知道程序怎样优雅退出吗?—— Go 开发中的“体面告别“全指南
go
光头闪亮亮3 天前
电子发票解析工具-golang服务端开发案例详解
go
Mgx3 天前
从“CPU 烧开水“到优雅暂停:Go 里 sync.Cond 的正确打开方式
go
GM_8284 天前
从0开始在Go当中使用Apache Thrift框架(万字讲解+图文教程+详细代码)
rpc·go·apache·thrift
Kratos开源社区4 天前
别卷 LangChain 了!Blades AI 框架让 Go 开发者轻松打造智能体
go·agent·ai编程
Kratos开源社区4 天前
跟 Blades 学 Agent 设计 - 01 用“提示词链”让你的 AI 助手变身超级特工
llm·go·agent