在我们开发golang程序时,很难保证研发同学不会写出”访问空指针”或者”除0″之类的错误代码逻辑。

当golang运行时执行到bug代码的时候,会抛出panic异常。

如果我们不主动进行捕获,那么异常就会沿着调用栈逐层向上传播,直到最终导致程序崩溃退出。

假设我正在开发一个web框架,那么就需要在框架层面做顶层的panic异常捕获,避免单个请求处理过程中的bug导致程序整体崩溃,做到请求之间互不影响是我们的目标。

未捕获panic的情况

下面演示一个”除0错误”,可以看到panic异常抛出并且导致程序崩溃:

Go
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()
}

程序输出:

Go
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异常
Go
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异常就停止了继续向上传播,程序便不会崩溃。

程序输出如下:

Go
1
runtime error: integer divide by zero

即recover成功抓到了panic并转换成了一个err。

错误的recover用法

一旦panic错误抛出,则golang不会继续执行后续代码,而是立即离开当前函数,因此只有利用defer才能确保recover捕获的执行。

下面是一个错误的recover用法,其并不会被执行,因此也无法捕获到panic:

Go
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)
}
}

代码输出:

Go
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默认的那样打印除调用栈,以便分析问题。

Go
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)这一行调用。

其输出如下:

Go
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而引入的栈,对分析问题没有意义:

Go
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()
}

这样的调用栈就没有多余信息了:

Go
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

 本节完。

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~