本文整理的 defer 的全场景使用情况。

defer 的执行顺序

Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说“栈的关系”,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

defer 与 return 谁先谁后

在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func f1() int {
x := 5
defer func() {
x++
}()
return x // 先把x赋值给f1函数的返回值,然后执行defer,所以x++对返回值没影响
}

func f2() (x int) {
defer func() {
x++
}()
return 5 // 先把5赋值给f2的返回值x,然后执行defer的x++,所以返回6
}

func f3() (y int) {
x := 5
defer func() { // 函数未传值,x引用了外部变量
x++
}()
return x // 先把x赋值给f3的返回值y,然后执行defer的x++,所以对返回值y没影响
}
func f4() (x int) {
defer func(x int) { // 函数传值,所以x是副本,此时x的值是0
x++ //改变的是函数中的x的副本
}(x)
return 5
}
func main() {
fmt.Println(f1()) // 5
fmt.Println(f2()) // 6
fmt.Println(f3()) // 5
fmt.Println(f4()) // 5
}

defer 遇见 panic

遇到 panic 时,会触发defer出栈执行 defer。在执行 defer 过程中:遇到 recover 则停止 panic,返回 recover 处继续往下执行。如果没有遇到 recover,执行完本协程的所有 defer 后,向 stderr 抛出 panic 信息。

defer 遇见 panic,但是并不捕获异常的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
defer_func()

fmt.Println("main 正常结束")
}

func defer_func() {
defer func() { fmt.Println("defer1") }()
defer func() { fmt.Println("defer2") }()

panic("异常信息") //触发defer出栈

defer func() { fmt.Println("defer3: 在panic之后,永远执行不到") }()
}

结果

1
2
3
4
defer2
defer1
panic: 异常信息
//... 异常堆栈信息

defer 遇见 panic,并捕获异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
defer_func()

fmt.Println("main 正常结束")
}

func defer_func() {

defer func() {
fmt.Println("defer1:捕获异常")
if err := recover(); err != nil {
fmt.Println(err)
}
}()

defer func() { fmt.Println("defer2") }()

panic("异常信息") //触发defer出栈

defer func() { fmt.Println("defer3: panic 之后, 永远执行不到") }()
}

结果

1
2
3
4
defer2
defer1:捕获异常
异常信息
main 正常结束

defer 中包含 panic

panic 仅有最后一个可以被 revover 捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main()  {

defer func() {
if err := recover(); err != nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()

defer func() {
panic("defer panic2")
}()

panic("panic1")
}

结果

1
defer panic2

当执行到panic("panic1")语句后,触发第一个panic,然后defer 顺序出栈执行,先执行panic("defer panic2"),这panic会覆盖的第一个panic,最后执行第二个defer,所以捕获了第二个panic。

defer 下的函数参数包含子函数

defer入栈时,需要把函数地址、函数形参一同压进栈

  1. 例子1:
1
2
3
4
5
6
7
8
9
10
11
func function(index int, value int) int {

fmt.Println(index)

return index
}

func main() {
defer function(1, function(3, 0))
defer function(2, function(4, 0))
}

结果

1
2
3
4
3
4
2
1

defer 一共会压栈两次,先进栈 1,后进栈 2。 那么在压栈function(1, function(3, 0))的时候,需要把函数地址函数形参一同压进栈,那么为了得到第二个参数的结果,所以就需要先执行 function(3, 0),将第二个参数算出,所以第一个结果是3;同理,function(2, function(4, 0))也一样。

  1. 闭包引用外部环境变量(创建新的副本)
1
2
3
4
5
6
7
8
9
10
11
12
func DeferFunc() (t int) {
defer func(i int) {
fmt.Println(i)
fmt.Println(t)
}(t)
t = 1
return 2
}

func main() {
DeferFunc()
}

结果

1
2
0
2

(1)初始化返回值 t 为零值 0

(2)defer入栈,同时需要把它对应的函数参数也入栈,因为t=0,所以i=0,所以第一个println会打印0

(3)t被赋值为1,然后return,t被赋值为2,最后第二个println打印2

  1. 闭包引用外部环境变量
1
2
3
4
5
6
7
8
func main() {
var whatever [6]struct{}
for i := range whatever {
defer func() {
fmt.Println(i)
}()
}
}

结果:

1
5,5,5,5,5

闭包=函数+引用环境,在 for 循环结束后,局部变量 i 的值已经是 5 了,并且defer的闭包是直接引用变量的 i,由于是引用,所以i跟着变化。

  1. defer链式调用,优先从左到右计算,只对最后的函数入栈
1
2
3
4
5
6
7
8
9
10
11
12
type temp struct{}

func (t *temp) Add(elem int) *temp {
fmt.Println(elem)
return &temp{}
}

func main() {
tt := &temp{}
defer tt.Add(1).Add(5).Add(2)
tt.Add(3)
}

结果

1
2
3
4
1
5
3
2

defer压栈只会压最后一个Add,前两个Add会在压栈前优先从左往右计算。

defer遇见exit

1
2
3
4
5
6
7
8
9
10
func test1() {
fmt.Println("test")
}

func main() {
fmt.Println("main start")
defer test1()
fmt.Println("main end")
os.Exit(0)
}

结果

1
2
main start
main end

如果在函数里是因为执行了os.Exit而退出,而不是正常return退出或者panic退出,那程序会立即停止,被defer的函数调用不会执行。

被defer的函数或方法的参数的值在执行到defer语句的时候就被确定下来了

1
2
3
4
5
6
func a() {
i := 0
defer fmt.Println(i) // 最终打印0
i++
return
}

因为执行defer时候,对i的值进行入栈操作,此时i是0,所以是0入栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func bar() (r int) {
defer func() {
r += 4
if recover() != nil {
r += 8
}
}()

var f func()
defer f() // f函数对象的值在此时就被定下来了,为nil,所以defer进去的f是nil
f = func() {
r += 2
}

return 1
}

func main() {
println(bar())
}

最终打印13