defer、panic 和 recover

defer、panic 和 recover


三者的关系大概如下:panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获。

defer 被调用的时候会调用 runtime.deferproc 定义一个延迟调用对象,然后在函数结束前,调用 runtime.deferreturn 来完成 defer 定义的函数的调用。defer结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
type _defer struct {
siz int32 // 参数的大小
started bool // 是否执行过了
sp uintptr // 栈指针
pc uintptr // 调用方的程序计时器
fn *funcval
_panic *_panic // defer中的panic
link *_defer // defer链表,函数执行流程中的defer,会通过 link这个 属性进行串联
fd unsafe.Pointer
varp uintptr
framepc uintptr
}

defer后面接的代码,在函数返回前才会被执行,例子:

1
2
3
4
5
6
7
8
func f() (result int) {

defer func() {
result++
}()
return 0
}
输出 1

以上值得注意的一点是,return XXX语句并不是一条原子语句,加上defer的定义之后,其实可以替换成以下内容:

1
2
3
返回值 = XXX
defer()
return 返回值

除非在执行 defer前就直接 return 了,那可以跳过defer。
还有值得注意的一点是,defer虽然是动态的,但是defer函数的参数值,是在申明defer时确定下来的,即如果使用某些值返回时,defer只认最开始被赋的值。对于这点,可以通过传入匿名函数来解决,例如:

1
2
3
4
5
6
7
8
9
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt)) //直接调用defer

time.Sleep(time.Second)
}

$ go run main.go
0s
1
2
3
4
5
6
7
8
9
func main() {
startedAt := time.Now()
defer func() { fmt.Println(time.Since(startedAt)) }()//使用匿名函数调用defer

time.Sleep(time.Second)
}

$ go run main.go
1s

panic的作用是中止当前程序,在被执行到时,直接使当前程序崩溃,然后返回。值得注意的一点是,当panic和defer同时存在时,在panic执行前,会先去执行defer的内容,随后再使程序崩溃,此时defer中的内容还是能够执行下去。defer 表达式的函数如果定义在 panic 后面,该函数在 panic 后就无法被执行到。

recover是 defer专属的对应函数,如果F的defer中无recover捕获,则将panic抛到G中,G函数会立刻终止,不会执行G函数内后面的内容,但不会立刻return,而调用G的defer…以此类推
F中出现panic时,F函数会立刻终止,不会执行F函数内panic后面的内容,但不会立刻return,而是调用F的defer,如果F的defer中有recover捕获,则F在执行完defer后正常返回,调用函数F的函数G继续正常执行,如果一直没有recover,抛出的panic到当前goroutine最上层函数时,程序直接异常终止。

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
func G() {
defer func() {
//goroutine外进行recover
if err := recover(); err != nil {
fmt.Println("捕获异常:", err)
}
fmt.Println("c")
}()
//创建goroutine调用F函数
go F()
time.Sleep(time.Second)
}

func F() {
defer func() {
fmt.Println("b")
}()
//goroutine内部抛出panic
panic("a")
}


//输出
b
panic: a

goroutine 5 [running]:
main.F()
/xxxxx/src/xxx.go:67 +0x55
created by main.main
/xxxxx/src/xxx.go:58 +0x51
exit status 2

recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成程序崩溃。recover返回的是interface{}类型而不是go中的 error 类型,如果外层函数需要调用err.Error(),会编译错误,也可能会在执行时panic。

应注意不要将 panic 返回给调用方,调用方关心的是错误而不是异常,将 panic 转换为 error,优雅处理异常

1
2
3
4
5
// 避免
func main() {
...
panic(404)
}
1
2
3
4
5
6
7
8
9
10
// 建议
func main() {

defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}
panic(404)
}