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