> **公众号「程序员祝融」,专注于后端技术,尤其是 Golang 及周边生态。**
在Go语言中,defer 定义为关键字, 在开发过程中使用非常高频,但是在使用中会有很多问题,在面试中也是高频考察点。今天我们结合案例来聊聊。
# 1 defer 作用
在 Golang 中,defer 比面向对象语言中的析构函数要强大很多,defer 还有错误捕获、修改函数返回值、资源释放等,defer 会在当前所在函数返回前执行传入的函数。例如:
```go
func CreateUsers(db *gorm.DB) error {
tx := db.Begin()
defer tx.Rollback()
if err := tx.Create(&Users{Name: "祝融"}).Error; err != nil {
return err
}
return tx.Commit().Error
}
```
注:调用 `tx.Commit()`之后执行 `tx.Rollback()`并不会影响已经提交事务。
# 2 defer 作用域
defer 作用域在当前函数和方法返回之前被调用。例如:
```go
package main
import "fmt"
func main() {
{
defer fmt.Println("defer done")
fmt.Println("code block done")
}
fmt.Println("main done...")
}
$ go run main
code block done
main done
defer done
```
我们会发现,传入的函数不是在退出代码块的作用域时执行的,defer 只会在当前函数和方法返回之前被调用。
# 3 defer 执行顺序
在 Golang 中,**defer** 的执行顺序是采用栈(stack)的方式。当你使用多个 **defer** 语句时,它们会按照后进先出(LIFO)的顺序执行。在一个函数生命周期内,优先调用后面的 defer 。例如:
```go
package main
import "fmt"
func main() {
defer funcA()
defer funcB()
defer funcC()
}
func funcA() {
fmt.Println("A")
}
func funcB() {
fmt.Println("B")
}
func funcC() {
fmt.Println("C")
}
$ go run main.go
C
B
A
```
图解:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09a6aef16e994932a5e0fcfa51790bad~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=2500&h=1004&s=144654&e=png&b=fffdfd)
## 3.1 defer、return 谁先执行?
在 Golang 中,return 比 defer 先执行,例如:
```go
package main
import "fmt"
func deferFunc() int {
fmt.Println("defer func done")
return 0
}
func returnFunc() int {
fmt.Println("return func done")
return 0
}
func returnAndDefer() int {
defer deferFunc()
return returnFunc()
}
func main() {
returnAndDefer()
}
$ go run main.go
return func done
defer func done
```
## 3.2 defer **影响主函数的具名返回值**
上面讲了 return 比 defer 先执行。
当主函数有返回值,且返回值有没有名字没有关系,defer 所作用的函数,即 defer 可能会影响主函数返回值。看一个例子:
```go
func main() {
fmt.Println(deferFuncReturn())
}
func deferFuncReturn() (j int) { // t初始化0且作用域为该函数全域
i := 1
defer func() {
j++
}()
return i
}
$ go run main.go
2
```
在举一个例子对比一下:
```go
func main() {
fmt.Println(deferFuncReturn())
}
func deferFuncReturn() int {
i := 1
defer func() {
i++
}()
return i
}
$ go run main.go
1
```
结论:当主函数有返回值 **,会在函数初始化时赋值为0,且其作用域为该函数全域,defer 会影响到该返回值。**
## 3.3 defer 偶遇 panic
在 Golang 中,执行过程中遇到 panic 错误时,遍历所有defer,强行 defer 出栈,并执行 defer。在执行过程中,
- 遇到 recover 捕获异常停止 panic,返回 recover 继续执行
- 未设置 recover 捕获异常,遍历完 defer 抛出 panic 信息
### 3.3.2 defer 未捕获 panic
```go
package main
import (
"fmt"
)
func main() {
defer_call()
fmt.Println("main done...")
}
func defer_call() {
defer func() {
fmt.Println("defer func 1")
}()
defer func() {
fmt.Println("defer func 2")
}()
// 触发defer出
panic("error")
defer func() {
fmt.Println("defer func 3: no exec")
}()
}
$ go run main.go
defer func 2
defer func 1
panic:error
... 堆栈error...
```
### 3.3.1 defer 捕获 panic
```go
package main
import (
"fmt"
)
func main() {
defer_call()
fmt.Println("main done...")
}
func defer_call() {
defer func() {
fmt.Println("defer func 1, 捕获异常")
if err := recover(); err != nil {
fmt.Println(err)
}
}()
defer func() {
fmt.Println("defer func 2, 没有捕获异常")
}()
// 触发defer出
panic("error")
defer func() {
fmt.Println("defer func 3: no exec")
}()
}
$ go run main.go
defer func 2, 没有捕获异常
defer func 1, 捕获异常
error
main done...
```
总结:从上面可以看出,程序执行中发生了 panic 异常,panic 前的 defer 一定能被执行到,所以我们一般用于关闭资源等,这样一定能保证资源能被关闭,避免一下问题。
### 3.3.3 defer 中含有 panic
Golang 中,panic 仅会被最后一个 revover 捕获。
```go
package main
import (
"fmt"
)
func main() {
defer func() {
if err := recover(); err != nil{
fmt.Println("err:", err)
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic2")
}()
panic("panic1")
}
$ go run main.go
err: defer panic2
```
在上面例子中,`panic("panic1")`先 触发 defer 强制出栈,第一个执行触发 `panic("defer panic2)"`异常,此时会覆盖前一个异常 `panic`,最后继续执行 defer, 最终被 `recover()`捕获住。
## 3.4 defer 函数嵌套子函数
分析下方代码,有 4 个函数,其中 x 为 1、2、3、4
```go
package main
import "fmt"
func f(x int, y int) int {
fmt.Println("x:", x)
return x
}
func main() {
defer f(1, f(3, 0))
defer f(2, f(4, 0))
}
```
先分析下执行顺序,有 2 个 defer,则会产生 **2** 次**入栈**操作,分别是 f1 、f2。
- f1 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出 (x:3)
- f2 入栈时,因为形参 y 是一个函数,则需要执行该函数,故执行一次输出(x:4)
main 函数执行完后,执行 defer 函数,所有 defer **出栈**,所有执行顺序为 f2、f1。所以程序最终输出的结果是:
```go
$ go run main.go
3
4
2
1
```
# 4 思考
一下所有函数传参均为 1。
## 4.1 defer_fun1
考点:res 的作用域
```go
func defer_fun1(x int) (res int) {
res = x
defer func() {
res += 3
}()
return res
}
```
题解:
- 函数有返回值,res 初始化为 0
- res = x 则 res = 1,defer 入栈
- return res 函数结束后,defer 出栈执行 res +3 = 4
最终函数返回结果为 4
## 4.2 defer_fun2
```go
func defer_fun2(x int) int {
res := x
defer func() {
res += 3
}()
return res
}
```
题解:
- res = x 则 res = 1,defer 入栈
- return res 程序结束后,此时函数返回值为1 ,但是返回值不是 res。
- defer 出栈执行 res +3 = 4
最终函数返回结果为 1
## 4.3 defer_fun3
```go
func defer_fun3(x int) (res int) {
defer func() {
res += x
}()
return 2
}
```
题解:
- 函数有返回值,res 初始化为 0
- x = 1, defer 入栈
- 函数结束res=2,执行 defer 出栈执行 res +1 = 3
最终函数返回结果为 3
## 4.4 defer_fun4
```go
func defer_fun4() (res int) {
t := 1
defer func(x int) {
fmt.Println("x:", x)
fmt.Println("res:", res)
}(t)
t = 2
return 4
}
```
题解:
- 函数有返回值,res 初始化为 0
- t 初始化为1
- defer 入栈,x 作为形参
- 执行t =2,此时主函数返回 4,则 res = 4
- 最后 defer 出栈,控制台输出 x: 1 res: 4
最终函数返回结果为 4。
![微信.png](https://static.golangjob.cn/230117/e6d7567719c440eb84c31be78d9b8159.png)
有疑问加站长微信联系(非本文作者))