Files
cs-note/hzh/GolangStar/Go语言基础/Go语言defer.md
T

159 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go基础语法, defer]
create time: 2026-06-07 15:00
---
# Go 语言 defer
## 概述
`defer` 是 Go 最优雅的特性之一——延迟函数执行直到外层函数返回。本文解释 defer 的执行顺序、参数求值时机,以及与 return 的交互关系。
## 正文
### 为什么 Go 需要 defer?
在 C++ 中你用 RAII(析构函数)管理资源,在 Java/Python 中有 `try-finally`。**Go 选择了更简洁的方案:`defer`**。它让资源清理代码紧跟在资源申请代码后面,而不是藏在函数的末尾。
> [!question] ❓ 思考
> 如果一个函数有多个返回路径(多个 `return`),如何保证每个路径都正确释放资源?
### defer 的基本用法
```go
func main() {
defer fmt.Println("第三行输出")
defer fmt.Println("第二行输出")
fmt.Println("第一行输出")
}
// 输出:
// 第一行输出
// 第二行输出
// 第三行输出
```
> [!note] 📝 LIFO 执行顺序
> defer 栈遵循 **后进先出**(Last In First Out)。最后一个 defer 最先执行。这类似于调用栈的行为。
### 核心场景:资源释放
这是 defer 最常见的用途:
```go
func CopyFile(dst, src string) (int64, error) {
s, err := os.Open(src)
if err != nil {
return 0, err
}
defer s.Close() // 无论多少条 return,s 都会被关闭
d, err := os.Create(dst)
if err != nil {
return 0, err
}
defer d.Close() // 同理
return io.Copy(d, s)
}
```
如果没有 defer,你需要在每个 return 前手动 Close,代码容易遗漏且冗长。
> [!tip] 💡 defer 的最佳实践
> - 打开资源后立即 defer 关闭
> - 不要过度使用——仅在真正需要"函数退出时清理"的场景用
> - 常见场景:文件关闭、锁释放、数据库连接归还
### ⚠️ defer 的三个经典陷阱
#### 陷阱 1:参数在 defer 声明时就求值
```go
num := 1
defer fmt.Printf("num is %d\n", num)
num = 2
// 输出:num is 1
```
**原因**:defer 的参数在声明语句执行时就已经确定并压入栈中了。后续修改 `num` 不影响已缓存的参数值。
```go
arr := [4]int{1, 2, 3, 4}
defer printArr(&arr) // 传入的是地址(指针值已确定)
arr[0] = 100
// 输出:100 2 3 4 — 地址没变,但指向的内容变了
```
> [!warning] ⚠️ 值 vs 指针
> - 传值 → 参数在 defer 声明时固化
> - 传指针 → 指针值固化,但指向的内容可变
#### 陷阱 2:return 不是原子操作
Go 的 `return` 实际上分三步执行:
```mermaid
graph LR
A["return 语句"] --> B["1. 设置返回值变量"]
B --> C["2. 执行 defer 栈"]
C --> D["3. 返回结果"]
```
这意味着:**如果使用了命名返回值,defer 可以修改返回值**。
```go
func foo() (res int) {
defer func() { res++ }()
return 1
}
fmt.Println(foo()) // 2 — defer 把 res 从 1 改成了 2
```
但如果返回值是匿名的:
```go
func bar() int {
defer func() {}()
return 1
}
fmt.Println(bar()) // 1 — defer 无法影响匿名返回值
```
#### 陷阱 3:nil panic 不会触发 defer 中的 recover
如果 defer 本身导致 panic(比如 nil 指针解引用),recover 可能捕获不到。确保 defer 体内的代码是安全的。
### defer + recover:异常捕获
配合 `recover()` 可以捕获 panic 并恢复程序:
```go
func safeDivide(a, b int) (result int, recovered string) {
defer func() {
if r := recover(); r != nil {
recovered = fmt.Sprintf("panic: %v", r)
}
}()
result = a / b
return
}
_, msg := safeDivide(10, 0)
fmt.Println(msg) // panic: runtime error: integer divide by zero
```
> [!info] ℹ️ recover 的限制
> - `recover()` 只能在 defer 中调用才有效
> - 它只能恢复同一个 goroutine 中的 panic
> - 不能恢复跨 goroutine 的 panic
### 关键要点总结
| 规则 | 说明 |
|------|------|
| 执行时机 | 外层函数 return 或 panic 时执行 |
| 执行顺序 | LIFO(后进先出) |
| 参数求值 | defer 声明时立即求值 |
| 可修改返回值 | 仅当使用命名返回值时 |
| 适用场景 | 资源清理、日志记录、性能监控 |