【golang】 panic 和 recover

Go 语言中两个经常成对出现的两个关键字 — panicrecover。这两个关键字与上一节提到的 defer 有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。

image.png

  • panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer
  • recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;

现象

  • panic 只会触发当前 Goroutine 的 defer
  • recover 只有在 defer 中调用才会生效;
  • panic 允许在 defer 中嵌套多次调用;

跨协程失效

panic 只会触发当前 Goroutine 的延迟函数调用。

1
2
3
4
5
6
7
8
9
go复制代码func main() {
defer println("in main")
go func() {
defer println("in goroutine")
panic("")
}()

time.Sleep(1 * time.Second)
}
1
2
3
4
go复制代码$ go run main.go
in goroutine
panic:
...

运行这段代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer

前面曾经介绍过 defer 关键字对应的 runtime.deferproc 会将延迟调用函数与调用方所在 Goroutine 进行关联。所以当程序发生崩溃时只会调用当前 Goroutine 的延迟调用函数也是非常合理的。

image.png

多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic 时也不应该执行其他 Goroutine 的延迟函数。

失效的崩溃恢复

在主程序中调用 recover 试图中止程序的崩溃,但是从运行的结果中能看出,下面的程序没有正常退出。

1
2
3
4
5
6
7
8
go复制代码func main() {
defer fmt.Println("in main")
if err := recover(); err != nil {
fmt.Println(err)
}

panic("unknown err")
}
1
2
3
4
5
6
7
8
less复制代码$ go run main.go
in main
panic: unknown err

goroutine 1 [running]:
main.main()
...
exit status 2

recover 只有在发生 panic 之后调用才会生效。然而在上面的控制流中,recover 是在 panic 之前调用的,并不满足生效的条件,所以需要在 defer 中使用 recover 关键字。

嵌套奔溃

Go 语言中的 panic 是可以多次嵌套调用的,如下所示的代码就展示了如何在 defer 函数中多次调用 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码func main() {
defer fmt.Println("in main 1")
defer func() {
defer func() {
panic("panic again and again")
}()
panic("panic again")
}()

defer fmt.Println("in main 2")

panic("panic once")
}
1
2
3
4
5
6
7
8
9
10
go复制代码$ go run main.go
in main 2
in main 1
panic: panic once
panic: panic again
panic: panic again and again

goroutine 1 [running]:
...
exit status 2

程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。

小结

分析程序的崩溃和恢复过程比较棘手,代码不是特别容易理解。

  1. 编译器会负责做转换关键字的工作;
    1. panicrecover 分别转换成 runtime.gopanicruntime.gorecover
    2. defer 转换成 runtime.deferproc 函数;
    3. 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
  2. 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
  3. 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
    1. 在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
    2. runtime.recovery 会根据传入的 pcsp 跳转回 runtime.deferproc
    3. 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
  4. 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

参阅

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%