1 基本概念
panic(宕机) 和 recover(恢复)是Go语言的两个内置函数,这两个内置函数用来处理Go的运行时错误(runtime errors)。panic 用来主动抛出异常,recover 用来捕获panic 抛出的异常。
panic 和 recover的函数原型如下:
panic(i interface{})
recover() interface{}
2 宕机(panic) — 程序终止运行
引发panic有两种情况:一种是程序主动调用panic()函数,另一种是程序产生运行时错误,由运行时检测并抛出。
示例1:程序主动调用panic,触发宕机,让程序崩溃。
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
funcC()
}
运行结果:
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
/home/wangxm/go_work/src/chapter05/demo.go:12
main.main()
/home/wangxm/go_work/src/chapter05/demo.go:20 +0x96
exit status 2
《代码分析》当在funcB中主动调用了panic 函数后,程序发生宕机直接退出了,同时输出了堆栈和goroutine相关信息,这让我们可以看到错误发生的位置。
【##】当panic()触发的宕机发生时,panic后面的代码将不会被执行,但是在panic()函数前面的已经执行过的defer语句依然会在宕机发生时执行defer中的延迟函数。
示例2:在宕机时触发defer语句延迟函数的执行。
func funcA() {
fmt.Println("func A")
}
func funcB() {
panic("panic in B")
}
func funcC() {
fmt.Println("func C")
}
func funcD() {
fmt.Println("func D")
}
func main() {
defer funcA()
defer funcC()
fmt.Println("this is main")
funcB()
defer funcD()
}
运行结果:
this is main
func C
func A
panic: panic in B
goroutine 1 [running]:
main.funcB(...)
/home/wangxm/go_work/src/chapter05/demo.go:12
main.main()
/home/wangxm/go_work/src/chapter05/demo.go:29 +0xca
exit status 2
《代码分析》从运行结果可以看出,当程序流程正常执行到funcB()函数的panic语句时,在panic()函数被执行前,defer语句会优先被执行,defer语句的执行顺序是先进后出,所以funcC()延迟函数先执行,funcA()后执行,当所有已注册的defer语句都执行完毕,才会执行panic()函数,触发宕机,程序崩溃退出,因此程序流程执行不到funcD()函数。
<提示> 发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈信息,直到被recover捕获或运行到最外层函数而退出。如本例中,程序从funcB()函数返回到上层的main()函数中,然后执行已注册的defer语句的延迟函数,最后从main函数中退出,并且打印了退出状态码的值为2。
【##】什么情况下主动调用panic() 函数抛出异常?
一般有下面两种情况:
(1)程序遇到了无法正常执行下去的错误,主动调用panic 函数结束程序运行。
(2)在调试程序时,通过主动调用panic 函数实现快速退出,panic打印出的堆栈信息能够更快地定位错误。
显然,我们是不希望程序崩溃的。那么,当程序在运行过程中突然发生了异常时,怎样“捕获”这个异常并处理这个异常避免程序崩溃呢?
3 恢复(recover) — 防止程序崩溃
recover()函数用来捕获或者说是拦截panic的,阻止panic继续向上层传递。无论是主动调用panic()函数触发的宕机还是程序在运行过程中由Runtime层抛出的异常,都可以配合defer 和 recover 实现异常捕获和恢复,让代码在发生panic后能够继续执行。
<提示> 在其他编程语言中,如Java,宕机往往以异常的形式存在。底层抛出异常,上层逻辑通过try...catch...机制捕获异常并处理,没有被捕获到的严重异常会导致程序崩溃,捕获的异常可以被处理,让代码可以继续执行。
Go语言没有异常系统,其使用panic触发宕机类似于其他语言的抛出异常,而recover的宕机恢复机制就对应try...catch机制。
【##】关于recover()函数的说明
recover()函数被调用后,会返回一个 interface{} 接口类型的返回值,如果返回值等于nil,说明没有触发panic;反之,说明程序发生了panic,就应该采取相应的措施。
示例1:使用recover捕获panic异常,恢复程序的运行。
func funcA() {
fmt.Println("func A")
}
func funcB() {
defer func(){
//捕获panic,并恢复程序使其继续运行
if err := recover(); err != nil {
fmt.Println("recover in funcB")
}
}()
panic("panic in B") //主动抛出异常
}
func funcC() {
fmt.Println("func C")
}
func funcD() {
fmt.Println("func D")
}
func main() {
defer funcA()
defer funcC()
fmt.Println("this is main")
funcB()
defer funcD()
}
运行结果:
this is main
recover in funcB
func D
func C
func A
《代码分析》当recover捕获到panic时,不会造成整个进程的崩溃,它会从触发panic的位置退出当前函数,然后继续执行后续代码。
【##】recover()的使用注意事项
- recover 必须搭配defer 语句使用,并且recover()函数必须在延迟函数内被调用执行才能正常工作。
- defer一定要在可能引发panic的语句之前定义。
func funcB() {
/*defer func(){
//捕获异常,并恢复程序使其继续运行
if err := recover(); err != nil {
fmt.Println("recover in funcB")
}
}()*/
defer recover() //捕获会失败
panic("panic in B") //主动抛出异常
}
4 panic和recover的关系
panic 和 defer语句的组合有如下几个特性:
(1)有panic,没recover,程序宕机。
(2)有panic,也有recover,程序不会宕机。执行完对应的defer后,从宕机点退出所在函数返回到主调函数中,继续执行后续代码。
<提示>
- 在panic触发的defer语句内,可以继续使用panic,进一步将错误外抛直至程序整体崩溃。
- 如果想在捕获panic时设置当前函数的返回值,可以对返回值使用命名返回值方式直接进行设置。
5 总结
Go语言提供了两种错误处理的方式,一种是借助 panic 和 recover 的抛出捕获机制,另一种是使用error接口错误类型。在实际的开发工作中,可以根据实际需求选择合适的错误处理方式。
参考
《Go语言从入门到进阶实战(视频教学版)》
《Go语言核心编程》
《Go并发编程实战(第2版)》
《Go语言学习笔记》