上一篇文章讲了一个核心关键字——点击了解defer
这一篇文章来聊一下Go当中的异常处理。
饭前开胃:
讲异常之前,需要先确定一个认知,异常 ≠ 错误, 错误常有而异常不常有,比如说我们打开文件出现错误,或者说支付一个已经关闭的订单等业务逻辑的错误。错误是可预期的,异常是不可预期的。就跟天气预报说要来台风了,我可以预期到要刮风下雨带把伞,但我预期不到我走在路上会被风刮下来的花盆给砸到了(doge) 异常虽然是“小众事件”,但不能就因此不进行处理。需要根据函数的角色和使用场景,考虑在函数内设置异常捕捉和恢复的环节。
正餐环节
1. Go中的异常与捕获:panic与recover
在Go中,有两种可能会触发panic,一种是程序运行的时候,另外一种是人为主动触发的,panic被触发之后的过程被Go语言称为panicking, panic是一个内置的函数,接收一个interface{}类型的值,也就是接收所有可能的输入。
在Go中,recover是Go内置的专门恢复panic的函数,它必须被放在一个defer函数中才能生效,如果recover捕捉到panic,会返回panic的具体内容,没有的话返回nil。重要的是, panic被recover捕捉到的话,panicking过程就会停止。程序得以继续运行。
涉及到panicking过程停止的panic传递又是怎样的呢? 继续往下看看
当一个函数发生了panic之后,若在当前函数中没有recover,会一直向外层传递直到主函数,如果迟迟没有recover的话,那么程序将终止。如果在过程中遇到了最近的recover,则将被捕获。捕获的函数不会再继续执行,而在调用链捕获的函数上面的函数能继续运行。看个具体的例子吧:
package main
import "fmt"
func testPanic1(){
fmt.Println("testPanic1上半部分")
testPanic2()
fmt.Println("testPanic1下半部分")
}
func testPanic2(){
defer func() {
recover()
}()
fmt.Println("testPanic2上半部分")
testPanic3()
fmt.Println("testPanic2下半部分")
}
func testPanic3(){
fmt.Println("testPanic3上半部分")
panic("在testPanic3出现了panic")
fmt.Println("testPanic3下半部分")
}
func main() {
fmt.Println("程序开始")
testPanic1()
fmt.Println("程序结束")
}
程序开始
testPanic1上半部分
testPanic2上半部分
testPanic3上半部分
testPanic1下半部分
程序结束
调用链:main-->testPanic1-->testPanic2-->testPanic3,panic3发现异常,没有recover处理,不再执行,往外层传递到panic2,panic2函数捕捉到panic,也不再继续执行了,而在调用链它的上面的函数能继续执行,所以下半部门只有panic1和main函数能得到正常执行。
所以recover和panic可以总结为以下两点:
- recover()只能恢复当前函数级或以当前函数为首的调用链中的函数中的panic(),恢复后调用当前函数结束,但是调用此函数的函数继续执行
- 函数发生了panic之后会一直向上传递,如果直至main函数都没有recover(),程序将终止,如果是碰见了recover(),将被recover捕获。
如果还不能理解的,那我讲个例子吧:假如老板、总经理和技术总监还有你一个小小的程序猿各司其职,有一天,因为你的技术失误导致公司损失了五十万,你承担不了这个后果,你就把锅往上层领导传递,技术总监感觉也处理不了,继续传递,总经理妥善的处理好了这个事情,就没有选择继续向老板传递了。同理,panic如果在调用链中的函数已经被捕捉到,那还没等它沿着函数栈向上走,就被消除了。
2. 多协程异常捕获
在Go中,并发编程十分常见,所以对于多协程的异常捕获还是有几个地方需要去注意一下:
attention1 : recover范围局限于当前goroutine, 不能捕获其他goroutine发生的panic
package main
import (
"fmt"
"time"
)
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("main recover:%v\n", e)
}
}()
go func() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("sub recover:%v\n", e)
}
}()
panic("sub func panic!!!")
fmt.Println("111") // 发生panic后,不会打印
}()
panic("main func panic!!!")
fmt.Println("222") // 发生panic后,不会打印
time.Sleep(2 * time.Second)
}
结果:
main recover:main func panic!!!
sub recover:sub func panic!!!
主函数goroutine中的recover只能捕获主goroutine中发生的panic,子goroutine只能捕获子goroutine发生的panic,当我们程序中有多个goroutine处理任务时,如果goroutine有可能发生panic,则在各个goroutine里都要捕获异常。当然每个协程都写个defer recover来捕获异常很繁琐,后面的文章会介绍处理办法,这里不过多纠缠。
attention2 : 一个recover只能捕获一次panic
package main
import (
"fmt"
)
func main() {
defer func() {
if e := recover(); e != nil {
fmt.Printf("recover:%v\n", e)
}
}()
panic("panic1")
panic("panic2")
fmt.Println("111") // 发生panic,不会打印
}
结果:
recover:panic1
3. 妥善应对panic
前面说了那么多,那么我们如何应对panic呢?最主要的还是要去评估程序对panic的忍受度,不同应用对异常引起的程序崩溃退出的忍受度是不一样的。比如,一个单次运行于控制台窗口中的命令行交互类程序,和一个常驻内存的后端 HTTP 服务器程序,对异常崩溃的忍受度就是不同的。像后端 HTTP 服务器程序这样的任务关键系统,我们就需要在特定位置捕捉并恢复 panic,以保证服务器整体的健壮度。
比如Go标准库中的http server: Go 标准库提供的 http server 采用的是,每个客户端连接都使用一个单独的 Goroutine 进行处理的并发处理模型。
为了不让一个协程发生panic产生的危害导致程序崩溃退出,Go 标准库在 serve 方法中加入了对 panic 的捕捉与恢复,下面是 serve 方法的部分代码片段:
// $GOROOT/src/net/http/server.go
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
defer func() {
if err := recover(); err != nil && err != ErrAbortHandler {
const size = 64 << 10
buf := make([]byte, size)
buf = buf[:runtime.Stack(buf, false)]
c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
}
if !c.hijacked() {
c.close()
c.setState(c.rwc, StateClosed, runHooks)
}
}()
... ...
}
serve 方法在一开始处就设置了 defer 函数,并在该函数中捕捉并恢复了可能出现的 panic。这样,即便处理某个客户端连接的 Goroutine 出现 panic,处理其他连接 Goroutine 以及 http server 自身都不会受到影响。
这种局部不要影响整体的异常处理策略,在很多并发程序中都有应用。并且,捕捉和恢复 panic 的位置通常都在子 Goroutine 的起始处,这样设置可以捕捉到后面代码中可能出现的所有 panic,就像 serve 方法中那样。
解释说明:第三部分内容来源于极客时间Tony Bai的Go专栏,链接
3,小结
- 文章主要承接上篇文章——优雅的defer,继续写了一篇关于Golang中的异常处理。第一部分写了抛出异常的panic以及捕获异常的recover。总结了以下两点:
- recover()只能恢复当前函数级或以当前函数为首的调用链中的函数中的panic(),恢复后调用当前函数结束,但是调用此函数的函数继续执行
- 函数发生了panic之后会一直向上传递,如果直至main函数都没有recover(),程序将终止,如果是碰见了recover(),将被recover捕获。
- 第二部分写了Golang并发异常的捕获,注意两个地方:
- recover范围局限于当前goroutine, 不能捕获其他goroutine发生的panic
- 一个recover只能捕获一次panic
- 最后摘取整合看到的好文章中的片段,如何妥善应对panic。