3. Go中panic与recover注意事项
2021-12-19 23:06
996 查看
1. 前言
Go 语言中两个经常成对出现的两个关键字 — panic 和 recover。这两个关键字与上一节提到的 defer 有紧密的联系,它们都是 Go 语言中的内置函数,也提供了互补的功能。
需要说明两点
panic
能够改变程序的控制流,调用panic
后会立刻停止执行当前函数的剩余代码,并在当前Goroutine
中递归执行调用方的defer
;-
立刻停止执行当前函数的剩余代码
- 当前goroutine中递归执行调用 defer
recover
可以中止panic
造成的程序崩溃。它是一个只能在defer
中发挥作用的函数,在其他作用域中调用不会发挥作用;-
recover只能与defer结合使用
2. 现象
- panic 只会触发当前goroutine的defer
- revoce 只有在defer中调用才能生效
- panic 允许在defer中嵌套多磁调用
2.1 跨协程失效
首先要介绍的现象是
panic只会触发当前 Goroutine 的延迟函数调用,通过如下所示的代码了解该现象:
package main import ( "fmt" "time" ) func main() { // 主线程中的defer函数并不会执行,因为子协程 panic后,主线程中的defer并不会执行 defer println("in main") go func() { defer println("in goroutine") fmt.Println("子协程running") panic("子协程崩溃") }() time.Sleep(1 * time.Second) }
# 输出 $ go run main.go 子协程running in goroutine panic: 子协程崩溃 goroutine 6 [running]: main.main.func1() ...
当运行这段代码时会发现 main 函数中的 defer 语句并没有执行,执行的只有当前 Goroutine 中的 defer。
2.2 不起作用的recover
初学 Go 语言工程师可能会写出下面的代码,在主程序中调用 recover 试图中止程序的崩溃,但是从运行的结果中也能看出,下面的程序没有正常退出。
package main import "fmt" func main() { defer fmt.Println("in main") if err := recover(); err != nil { fmt.Println(err) } panic("unknown err") }
# 输出 $ go run main.go in main panic: unknown err goroutine 1 [running]: main.main() D:/gopath/src/Go_base/lesson/panic/demo5.go:11 +0x125
仔细分析一下这个过程就能理解这种现象背后的原因,**recover 只有在发生 panic 之后调用才会生效。**然而在上面的控制流中,recover 是在 panic 之前调用的,并不满足生效的条件,所以我们需要在 defer 中使用 recover 关键字。
正确的写法应该是这样:
package main import "fmt" func main() { defer fmt.Println("in main") defer func() { if err := recover(); err != nil { fmt.Println("occur error") fmt.Println(err) } }() panic("unknown err") }
2.3 嵌套使用panic
panic是可以多次嵌套调用的。,如下所示的代码就展示了如何在 defer 函数中多次调用 panic:
package main import "fmt" func main() { defer fmt.Println("in main") defer func() { defer func() { panic("panic again and again") }() panic("panic again") }() panic("panic once") }
# 输出 $ go run main.go in main panic: panic once panic: panic again panic: panic again and again goroutine 1 [running]: main.main.func1.1() ...
从上述程序输出的结果,我们可以确定程序多次调用 panic 也不会影响 defer 函数的正常执行,所以使用 defer 进行收尾工作一般来说都是安全的。
3. panic数据结构
panic关键字在源代码是由数据结构
runtime._panic表示的。每当调用
panic都会创建一个如下所示的数据结构存储相关信息:
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool pc uintptr sp unsafe.Pointer goexit bool }
argp
是指向 defer 调用时参数的指针;arg
是调用panic
时传入的参数;link
指向了更早调用的runtime._panic
结构;recovered
表示当前runtime._panic
是否被recover
恢复;aborted
表示当前的 panic 是否被强行终止;
具体的panic 程序崩溃与恢复崩溃原理在此不做延伸, 可参考panic与recover
4. 小结
简单总结一下程序崩溃和恢复的过程:
- 编译器会负责做转换关键字的工作
-
将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;
- 将 defer 转换成 runtime.deferproc 函数
- 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
- 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
- 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;
-
在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;
- runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc;
- 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
- 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;
5. 参考
相关文章推荐
- Go的异常处理 defer, panic, recover
- go 错误处理panic recover
- go里面select-case和time.Ticker的使用注意事项
- go写并发注意事项
- Go的异常处理 defer, panic, recover
- Go语言中切片使用的注意事项小结
- go注意事项
- [Go] 复合类型(数组、切片、字典、结构体)变量的 初始化 及 注意事项
- Go-defer,panic,recover
- [Go] 如何正确地 抛出 错误 和 异常(error/panic/recover)?
- Go语言中的 panic 和 recover
- Ubuntu16.04 部署配置GO语言开发环境 & 注意事项
- 【Go入门教程5】流程(if、goto、for、switch)和函数(多个返回值、变参、传值与传指针、defer、函数作为值/类型、Panic和Recover、main函数和init函数、import)
- Go基础系列:defer、panic和recover
- go接口扩展注意事项
- go error panic deffer recover
- Go语言学习笔记 --- 指针和自增自减运算及相关注意事项
- Go错误定义和处理error&defer&panic&recover
- Go语言中使用panic和recover简化错误处理
- 谈一谈Go的异常处理机制——panic和recover的使用和原理