avatar

目录
有关 defer 的故事

最近写涉及链接数据库的单测比较多,很多地方用到了defer, 之前只知道用,大体有个了解,空闲时间看了几篇博文,简单整理一下。

调用时间

defer的调用时机,不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用。

go
1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
{
defer fmt.Println("defer runs")
fmt.Println("----------")
}

fmt.Println("main")
}

$ go run main.go
----------
main
defer runs

执行顺序

go
1
2
3
4
5
6
7
8
9
10
11
12
   func main() {
defer func() {
println("defer 1")
}()
defer func() {
println("defer 2")
}()
}

$ go run main.go
defer 2
defer 1

明显的后进先出

panic 与 defer

如果当 panic 遇上了deferdefer中的内容,一定会被执行吗?

先说结论:panic仅保证当前goroutine下的defer都会被调到,但不保证其他协程的defer也会调到。
如果是在同一goroutine下的调用者的defer,那么可以一路回溯回去执行;但如果是不同goroutine,那就不做保证了。

可以看一下下面的示例代码

go
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
35
36
37
38
39
package main

import (
"os"
"fmt"
"time"
)

func main() {
defer fmt.Println("defer main")
var user = os.Getenv("USER_")
go func() {
defer fmt.Println("defer caller")
func() {
defer func() {
fmt.Println("defer here")
}()

if user == "" {
panic("should set user env.")
}
}()
}()

time.Sleep(1 * time.Second)
}

$ go run main.go
defer here
defer caller
panic: should set user env.

goroutine 18 [running]:
main.main.func1.1(0x0, 0x0)
/tmp/sandbox607792549/prog.go:20 +0x79
main.main.func1(0x0, 0x0)
/tmp/sandbox607792549/prog.go:22 +0x9b
created by main.main
/tmp/sandbox607792549/prog.go:12 +0xb1

如上例,main中增加一个defer,但会睡1s,在这个过程中panic了,还没等到main睡醒,进程已经退出了。
因此maindefer不会被调到;而跟panic同一个goroutinedefer caller还是会打印的,并且其打印在defer here之后。

内部实现

Go 运行时使用链表来实现 LIFO,一个 defer 结构体持有指向下一个要被执行的 defer 结构体的指针

go
1
2
3
4
5
6
7
8
9
type _defer struct {
siz int32 // 是参数和结果的内存大小
started bool
sp uintptr // 栈指针的计数器
pc uintptr // 调用方的计数器
fn *funcval // 传入的函数
_panic *_panic // 触发延迟调用的结构体,可以为空
link *_defer // 下一个要被执行的延迟函数
}

所有的结构体都通过link字段,串联成链表。

panic 与 recover

panic 会停掉当前正在执行的程序(不只是当前协程),会处理完当前goroutine已经defer挂上去的任务,执行完毕后再退出整个程序。

defer中通过recover拿到panic,可以达到类似try....catch的效果。

recover是如何实现的?

go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}

recover会先检查当先线程是否在panic流程,如果不是,直接返回nil。所以在普通流程调用recover除了耗费 cps,并不会有什么实际作用。

文章作者: Viola Tangxl
文章链接: https://violatangxl.github.io/2020/09/26/golang-defer/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 椰子是只猫
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论