golang – 利用recover捕获panic异常
在我们开发golang程序时,很难保证研发同学不会写出”访问空指针”或者”除0″之类的错误代码逻辑。
当golang运行时执行到bug代码的时候,会抛出panic异常。
如果我们不主动进行捕获,那么异常就会沿着调用栈逐层向上传播,直到最终导致程序崩溃退出。
假设我正在开发一个web框架,那么就需要在框架层面做顶层的panic异常捕获,避免单个请求处理过程中的bug导致程序整体崩溃,做到请求之间互不影响是我们的目标。
未捕获panic的情况
下面演示一个”除0错误”,可以看到panic异常抛出并且导致程序崩溃:
1 2 3 4 5 6 7 8 9 10 11 12 |
package main import "fmt" func stupidCode() { n := 0 fmt.Println(1/ n) } func main() { stupidCode() } |
程序输出:
1 2 3 4 5 6 7 8 9 |
panic: runtime error: integer divide by zero goroutine 1 [running]: main.stupidCode() /Users/liangdong/go/src/blog/demo1/main.go:7 +0x11 main.main() /Users/liangdong/go/src/blog/demo1/main.go:11 +0x20 Process finished with exit code 2 |
golang默认会把崩溃的堆栈输出到屏幕上。
一个健壮的web框架不应该因为某一个接口的逻辑不严谨而导致程序崩溃,因此必须对其进行异常捕获。
利用recover捕获panic异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import "fmt" func stupidCode() { n := 0 fmt.Println(1/ n) } func main() { defer func() { if err := recover(); err != nil { fmt.Println(err) } }() stupidCode() } |
因为stupidCode内部会抛出panic异常沿着调用栈向上传播,stupidCode的上一层就是main。
为了在main方法中捕获到stupidCode抛来的panic异常,我们必须利用defer进行捕获,它将在离开main方法前被执行。
在defer中调用recover可以抓住panic异常,这样panic异常就停止了继续向上传播,程序便不会崩溃。
程序输出如下:
1 |
runtime error: integer divide by zero |
即recover成功抓到了panic并转换成了一个err。
错误的recover用法
一旦panic错误抛出,则golang不会继续执行后续代码,而是立即离开当前函数,因此只有利用defer才能确保recover捕获的执行。
下面是一个错误的recover用法,其并不会被执行,因此也无法捕获到panic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package main import "fmt" func stupidCode() { n := 0 fmt.Println(1/ n) } func main() { stupidCode() if err := recover(); err != nil { fmt.Println(err) } } |
代码输出:
1 2 3 4 5 6 7 |
panic: runtime error: integer divide by zero goroutine 1 [running]: main.stupidCode() /Users/liangdong/go/src/blog/demo1/main.go:7 +0x11 main.main() /Users/liangdong/go/src/blog/demo1/main.go:11 +0x26 |
打印出panic调用栈
我们在recover抓到异常后,需要像golang默认的那样打印除调用栈,以便分析问题。
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 |
package main import ( "fmt" "runtime" ) func stupidCode() { n := 0 fmt.Println(1/ n) } func main() { defer func() { if err := recover(); err != nil { for i := 0; ; i++ { pc, file, line, ok := runtime.Caller(i) if !ok { break } fmt.Println(pc, file, line) } } }() stupidCode() } |
这里调用runtime.Caller即可获取每一层调用栈,数字0表示当前层级,也就是runtime.Caller(0)这一行调用。
其输出如下:
1 2 3 4 5 6 7 |
17404968 /Users/liangdong/go/src/blog/demo1/main.go 17 16944753 /usr/local/go/src/runtime/panic.go 679 16941706 /usr/local/go/src/runtime/panic.go 178 17404496 /Users/liangdong/go/src/blog/demo1/main.go 10 17404583 /Users/liangdong/go/src/blog/demo1/main.go 25 16952509 /usr/local/go/src/runtime/proc.go 203 17117312 /usr/local/go/src/runtime/asm_amd64.s 1357 |
调用栈的第一行是最近的一个调用,也就是defer函数这一层栈。
到达defer函数前有2层golang内部的panic的调用栈,然后就是抛出异常的调用栈了:
17404496 /Users/liangdong/go/src/blog/demo1/main.go 10
fmt.Println(1 / n)这行代码引起的。
所以,实际上我们输出调用栈可以直接跳过前3层,它们都是因为panic与捕获panic而引入的栈,对分析问题没有意义:
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 |
package main import ( "fmt" "runtime" ) func stupidCode() { n := 0 fmt.Println(1/ n) } func main() { defer func() { if err := recover(); err != nil { for i := 3; ; i++ { pc, file, line, ok := runtime.Caller(i) if !ok { break } fmt.Println(pc, file, line) } } }() stupidCode() } |
这样的调用栈就没有多余信息了:
1 2 3 4 |
17404496 /Users/liangdong/go/src/blog/demo1/main.go 10 17404583 /Users/liangdong/go/src/blog/demo1/main.go 25 16952509 /usr/local/go/src/runtime/proc.go 203 17117312 /usr/local/go/src/runtime/asm_amd64.s 1357 |
本节完。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
