vault backup: 2026-06-07 12:14:39
This commit is contained in:
+112
-255
@@ -1,301 +1,158 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go基础语法
|
||||
- defer
|
||||
tags: [go, golang, go基础语法, defer]
|
||||
create time: 2026-06-07 15:00
|
||||
---
|
||||
|
||||
# Go语言defer
|
||||
# Go 语言 defer
|
||||
|
||||
## defer关键字
|
||||
`defer`顾名思义,延迟。它是go语言中的一个关键字,主要用在函数或方法前面,作用是用于函数和方法的延迟调用,在语法上,`defer`与普通的函数调用没有什么区别。
|
||||
在使用上非常简单,只需要弄清楚以下几点即可:
|
||||
1. 延迟的函数的什么时候被调用?
|
||||
1. 函数`return`的时候
|
||||
2. 发生`panic`的时候
|
||||
2. 延迟调用的语法规则
|
||||
1. `defer`关键字后面表达式必须是函数或者方法调用
|
||||
2. 延迟内容不能被括号括起来
|
||||
## 概述
|
||||
|
||||
## defer执行顺序
|
||||
上一小节说到`defer`关键字后面的函数调用会在函数`return`或者发生`panic`的时候执行,这个在单个`defer`的时候很好理解,但当一个函数中有多个`defer`的时候,他们的顺序是怎么样的呢?
|
||||
`defer`语句的执行顺序是先进后出LIFO。下面请看具体例子:
|
||||
```go
|
||||
package main
|
||||
`defer` 是 Go 最优雅的特性之一——延迟函数执行直到外层函数返回。本文解释 defer 的执行顺序、参数求值时机,以及与 return 的交互关系。
|
||||
|
||||
import "fmt"
|
||||
## 正文
|
||||
|
||||
func defer1() {
|
||||
fmt.Println("defer1")
|
||||
}
|
||||
### 为什么 Go 需要 defer?
|
||||
|
||||
func defer2() {
|
||||
fmt.Println("defer2")
|
||||
}
|
||||
在 C++ 中你用 RAII(析构函数)管理资源,在 Java/Python 中有 `try-finally`。**Go 选择了更简洁的方案:`defer`**。它让资源清理代码紧跟在资源申请代码后面,而不是藏在函数的末尾。
|
||||
|
||||
func defer3() {
|
||||
fmt.Println("defer3")
|
||||
}
|
||||
> [!question] ❓ 思考
|
||||
> 如果一个函数有多个返回路径(多个 `return`),如何保证每个路径都正确释放资源?
|
||||
|
||||
### defer 的基本用法
|
||||
|
||||
```go
|
||||
func main() {
|
||||
defer defer1()
|
||||
defer defer2()
|
||||
defer defer3()
|
||||
defer fmt.Println("第三行输出")
|
||||
defer fmt.Println("第二行输出")
|
||||
fmt.Println("第一行输出")
|
||||
}
|
||||
// 输出:
|
||||
// 第一行输出
|
||||
// 第二行输出
|
||||
// 第三行输出
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
defer3
|
||||
defer2
|
||||
defer1
|
||||
```
|
||||
可以看到执行顺序跟栈是一样的,先调用,后执行
|
||||
|
||||
## defer的使用场景
|
||||
通过前面的小节我们知道了`defer`关键字主要是用于延迟调用,那么什么场景下需要我们用到延迟调用了,有过Go语言基础的同学在一些代码中经常看到`defer`关键字。`defer`关键字一般用在以下两个场景中
|
||||
> [!note] 📝 LIFO 执行顺序
|
||||
> defer 栈遵循 **后进先出**(Last In First Out)。最后一个 defer 最先执行。这类似于调用栈的行为。
|
||||
|
||||
### 核心场景:资源释放
|
||||
|
||||
这是 defer 最常见的用途:
|
||||
|
||||
### 资源的释放
|
||||
通过`defer`延迟调用机制,我们可以简洁优雅处理资源回收问题,从而避免在复杂的代码逻辑情况下,遗漏相关的资源回收问题,用的比较多的就是类似网络连接,数据库连接,以及文件句柄的资源的释放。
|
||||
看看下面一个复制文件的函数:
|
||||
```go
|
||||
func CopyFile(dstFile, srcFile string) (wr int64, err error) {
|
||||
src, err := os.Open(srcFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
dst, err := os.Create(dstFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
wr, err = io.Copy(dst, src)
|
||||
dst.Close()
|
||||
src.Close()
|
||||
return
|
||||
}
|
||||
```
|
||||
仔细看这段代码,其实是有问题的,比如当地6行执行失败,程序直接返回了,但我们并没有关闭前面打开的文件资源src,这样就造成了资源的浪费。
|
||||
那么用`defer`关键字,我们可以怎么做呢?
|
||||
```go
|
||||
func CopyFile(dstFile, srcFile string) (wr int64, err error) {
|
||||
src, err := os.Open(srcFile)
|
||||
func CopyFile(dst, src string) (int64, error) {
|
||||
s, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
return 0, err
|
||||
}
|
||||
defer src.Close()
|
||||
defer s.Close() // 无论多少条 return,s 都会被关闭
|
||||
|
||||
dst, err := os.Create(dstFile)
|
||||
d, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
return 0, err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
wr, err = io.Copy(dst, src)
|
||||
return wr, err
|
||||
defer d.Close() // 同理
|
||||
|
||||
return io.Copy(d, s)
|
||||
}
|
||||
```
|
||||
只要我们正确打开了某个资源,比如`src`和`dst`,没返回`err`的情况下,都可以用`defer`延迟调用来关闭资源,注意,这是`go`语言中非常常见的一种资源关闭方式。
|
||||
|
||||
### 配合recover一起处理panic
|
||||
`defer`另一个常用的地方就是在处理程序`panic`的时候,关于程序的异常捕获我们将在下一个小节讲到,这里大家可以先了解一下,`go`语言中用`panic`来抛出异常,用`recover`来捕获异常,所以当我们的程序出现异常的时候,我们需要知道是发生了什么异常的时候,就可以用`defer recover`来捕获异常
|
||||
如果没有 defer,你需要在每个 return 前手动 Close,代码容易遗漏且冗长。
|
||||
|
||||
> [!tip] 💡 defer 的最佳实践
|
||||
> - 打开资源后立即 defer 关闭
|
||||
> - 不要过度使用——仅在真正需要"函数退出时清理"的场景用
|
||||
> - 常见场景:文件关闭、锁释放、数据库连接归还
|
||||
|
||||
### ⚠️ defer 的三个经典陷阱
|
||||
|
||||
#### 陷阱 1:参数在 defer 声明时就求值
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println(r)
|
||||
}
|
||||
}()
|
||||
a := 1
|
||||
b := 0
|
||||
fmt.Println("result:", a/b)
|
||||
}
|
||||
num := 1
|
||||
defer fmt.Printf("num is %d\n", num)
|
||||
num = 2
|
||||
// 输出:num is 1
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
result: runtime error: integer divide by zero
|
||||
```
|
||||
可以看到,程序并没有输出`result`,这是因为我们尝试对一个除数为0的数做除法,这是不允许的,所以程序回panic,但我们用`defer`在程序发生`panic`的时候捕获了这个异常,打印出异常信息:`runtime error: integer divide by zero`。
|
||||
|
||||
## defer与return
|
||||
前面第一小节我们介绍过`defer`函数的执行是在`return`的时候,那么在具体一点,在`return`的时候,`defer`具体做了什么?又会带来什么结果?这是一个非常值得探讨的问题,也是面试官在面试中经常会问的问题,往往通过这个问题就可一看出一个面试者对`go`语言掌握的扎不扎实。
|
||||
1. 例子1:
|
||||
**原因**:defer 的参数在声明语句执行时就已经确定并压入栈中了。后续修改 `num` 不影响已缓存的参数值。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
|
||||
func deferRun() {
|
||||
var num = 1
|
||||
defer fmt.Printf("num is %d", num)
|
||||
|
||||
num = 2
|
||||
return
|
||||
}
|
||||
|
||||
func main(){
|
||||
deferRun()
|
||||
}
|
||||
arr := [4]int{1, 2, 3, 4}
|
||||
defer printArr(&arr) // 传入的是地址(指针值已确定)
|
||||
arr[0] = 100
|
||||
// 输出:100 2 3 4 — 地址没变,但指向的内容变了
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
num is 1
|
||||
```
|
||||
为什么?
|
||||
延迟函数 `defer fmt.Printf("num is %d", num)` 的参数`num`在`defer`语句出现的时候就已经确定,`num=1`,所以不管后面怎么修改 `a` 的值,最终调用`defer`函数传递给`defer`函数的参数已经固定是`1`了,不会再变化。
|
||||
|
||||
2. 例子2:
|
||||
> [!warning] ⚠️ 值 vs 指针
|
||||
> - 传值 → 参数在 defer 声明时固化
|
||||
> - 传指针 → 指针值固化,但指向的内容可变
|
||||
|
||||
#### 陷阱 2:return 不是原子操作
|
||||
|
||||
Go 的 `return` 实际上分三步执行:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["return 语句"] --> B["1. 设置返回值变量"]
|
||||
B --> C["2. 执行 defer 栈"]
|
||||
C --> D["3. 返回结果"]
|
||||
```
|
||||
|
||||
这意味着:**如果使用了命名返回值,defer 可以修改返回值**。
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
deferRun()
|
||||
func foo() (res int) {
|
||||
defer func() { res++ }()
|
||||
return 1
|
||||
}
|
||||
|
||||
func deferRun() {
|
||||
var arr = [4]int{1, 2, 3, 4}
|
||||
defer printArr(&arr)
|
||||
|
||||
arr[0] = 100
|
||||
return
|
||||
}
|
||||
|
||||
func printArr(arr *[4]int) {
|
||||
for i := range arr {
|
||||
fmt.Println(arr[i])
|
||||
}
|
||||
}
|
||||
fmt.Println(foo()) // 2 — defer 把 res 从 1 改成了 2
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
100
|
||||
2
|
||||
3
|
||||
4
|
||||
```
|
||||
为什么?
|
||||
通过前一个地址,我们知道在defer出现的时候,参数已经确定,但是这里传递的是地址,地址没变,但是地址对应的内容被修改了,所以输出会被修改。
|
||||
|
||||
3. 例子3:
|
||||
但如果返回值是匿名的:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
res := deferRun()
|
||||
fmt.Println(res)
|
||||
func bar() int {
|
||||
defer func() {}()
|
||||
return 1
|
||||
}
|
||||
fmt.Println(bar()) // 1 — defer 无法影响匿名返回值
|
||||
```
|
||||
|
||||
func deferRun() (res int) {
|
||||
num := 1
|
||||
|
||||
defer func() {
|
||||
res++
|
||||
}()
|
||||
|
||||
return num
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
2
|
||||
```
|
||||
为什么?
|
||||
这是一个非常经典的例子,要想准确的的只程序的执行结果,需要我们对函数return的执行有一个细致的了解。其实函数的return并非一个原子操作,return的过程可以被分解为以下三步:
|
||||
1. 设置返回值
|
||||
2. 执行defer语句
|
||||
3. 将结果返回
|
||||
所以,在本例中,第一步是将`result`的值设置为`num`,此时还未执行`defer`,`num`的值是`1`,所以`result`被设置为`1`,然后再执行`defer`语句将`result+1`,最终将`result`返回,所以会打印出`2`。
|
||||
#### 陷阱 3:nil panic 不会触发 defer 中的 recover
|
||||
|
||||
如果 defer 本身导致 panic(比如 nil 指针解引用),recover 可能捕获不到。确保 defer 体内的代码是安全的。
|
||||
|
||||
### defer + recover:异常捕获
|
||||
|
||||
配合 `recover()` 可以捕获 panic 并恢复程序:
|
||||
|
||||
4. 例子4:
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
res := deferRun()
|
||||
fmt.Println(res)
|
||||
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
|
||||
}
|
||||
|
||||
func deferRun() int {
|
||||
var num int
|
||||
defer func() {
|
||||
num++
|
||||
}()
|
||||
|
||||
return 1
|
||||
}
|
||||
_, msg := safeDivide(10, 0)
|
||||
fmt.Println(msg) // panic: runtime error: integer divide by zero
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
1
|
||||
```
|
||||
为什么?
|
||||
本例和前面的区别返回值是匿名的,但是我们可以同样运用上面的思路,自己创建一个返回值,这里假设为`res`,运用前面的思路分析,第一步将`res`设置为1,第二步执行`defer`将`num+1`,第三步将`res`返回,所以最终结果是`1`。
|
||||
|
||||
5. 例子5:
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
res := deferRun()
|
||||
fmt.Println(res)
|
||||
}
|
||||
|
||||
func deferRun() int {
|
||||
num := 1
|
||||
defer func() {
|
||||
num++
|
||||
}()
|
||||
|
||||
return num
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
1
|
||||
```
|
||||
同样的思路不难分析:自己创建一个返回值,这里假设为`res`,第一步将`res`设置为`num`,所以`res`的值为`1`,第二步执行`defer`将`num+1`,此时`num`为`2`,但是`res`为`1`,第三步将`res`返回,所以最终结果是`1`。
|
||||
|
||||
6. 例子6:
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
res := deferRun()
|
||||
fmt.Println(res)
|
||||
}
|
||||
|
||||
func deferRun() (res int) {
|
||||
num := 1
|
||||
defer func() {
|
||||
num++
|
||||
}()
|
||||
|
||||
return num
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
1
|
||||
```
|
||||
不难分析运行结果还是1,同样的三步分析法,因为`defer`改变的是`num`的值,而不是改变的`res`的值,所以结果不会变,不过`defer`函数里变为`res++`,那么结果就是`2`了。
|
||||
所以,当我们碰到defer与return确定最终的返回值,可以总结为以下两点:
|
||||
1. defer 定义的延迟函数的参数在`defer`语句出时就已经确定下来了
|
||||
2. `return`不是原子级操作,执行过程是: 设置返回值—>执行`defer`语句—>将结果返回
|
||||
|
||||
> [!info] ℹ️ recover 的限制
|
||||
> - `recover()` 只能在 defer 中调用才有效
|
||||
> - 它只能恢复同一个 goroutine 中的 panic
|
||||
> - 不能恢复跨 goroutine 的 panic
|
||||
|
||||
### 关键要点总结
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 执行时机 | 外层函数 return 或 panic 时执行 |
|
||||
| 执行顺序 | LIFO(后进先出) |
|
||||
| 参数求值 | defer 声明时立即求值 |
|
||||
| 可修改返回值 | 仅当使用命名返回值时 |
|
||||
| 适用场景 | 资源清理、日志记录、性能监控 |
|
||||
|
||||
Reference in New Issue
Block a user