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)
}

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment