我们的应用程序常常会出现异常,包括由运行时检测到的异常或者应用开发者自己抛出的异常。
以下是一段简单的panic和recover使用示例:
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
/*defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()*/
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
fmt.Println("Printing in g", i)
panic(i)
fmt.Println("After panic in g", i)
}
我们先把defer recover部分注释,运行结果如下:
Calling g.
Printing in g 0
panic: 0
goroutine 1 [running]:
main.g(0x4b14a0)
/tmp/sandbox2444947193/prog.go:18 +0x94
main.f()
/tmp/sandbox2444947193/prog.go:12 +0x5d
main.main()
/tmp/sandbox2444947193/prog.go:6 +0x19
Program exited.
可以看到程序运行到g方法的第二行时,产生的panic导致进程异常退出
,后续的代码都没有执行。
再把recover注释部分打开,运行结果为:
Calling g.
Printing in g 0
Recovered in f 0
Returned normally from f.
Program exited.
f方法中的recover捕获了panic,打印了panic传递的参数,并且main方法是正常返回的。g方法panic之后的代码没有执行。
panic是go的内置函数,它可以终止程序的正常执行流程并发出panic。
比如:当函数F调用panic,F的执行将被终止,并返回到调用者。对调用者而言,F就像调用者直接调用了panic。该过程一直跟随堆栈向上,直到当前goroutine中的所有函数都返回,此时 程序崩溃
panic可以通过直接调用panic产生。同时也可能由运行时的错误所产生,例如数组越界访问。
recover是go语言的内置函数,它的唯一作用是可以从panic中重新控制goroutine的执行。recover必须通过defer来运行
。
在正常的执行流程中,调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panic,recover将会捕获到panic抛出的信息,同时恢复其正常的执行流程。
小结
我们可以手动调用内置函数panic,但是那些空指针、数组越界等运行时panic是如何被检测到的,下面针对这一问题做一些代码调试
测试代码
package main
func main() {
a := 0
testDivide(a) //除零
//testOutRange() //越界
//testNil() //空指针
//panic("666") //自定义panic
}
func testDivide(a int) {
b := 10 / a
_ = b
}
func testOutRange() {
var a []int
a[0] = 2
}
func testNil() {
var a *int
*a = 1
}
调试代码
与linux平台下的gdb调试工具类似,dlv用来调试go语言编写的程序。
dlv是一个命令行工具,它包含了多个调试命令,例如运行程序、下断点、打印变量、step in、step out等。我们常用的go语言编辑器,如vscode、golang等的可视化调试也是调用dlv。
找出panic是怎么产生的:
这里我们先给出结论,具体调试过程产生的代码,请往下看
调试自定义panic方法:
使用dlv调试testDivide中的代码,有以下几个关键步骤:
所以其实panic方法实际调用了runtime.gopanic
小结
由于panic和defer有着难解难分的关系,我们先了解一下defer。
defer定义的官翻:
defer语句将函数调用保存到一个列表上。保存的调用列表在当前函数返回前执行。Defer通常用于简化执行各种清理操作的函数。
通俗地说,就是defer保证函数调用不管在什么情况下(即使当前函数发生panic),在当前函数返回前必然执行。另外defer的函数调用符合先进后出的规则,即先defer的函数后执行。
我们看一个示例程序,它是第一节示例程序的升级版本,方法g中会调用自身:
package main
import "fmt"
func main() {
defer func() {
fmt.Println("defer in main")
}()
f()
fmt.Println("Returned normally from f.")
}
func f() {
/*defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()*/
defer func() {
fmt.Println("defer in f")
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
程序运行结果如下:
Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
defer in f
defer in main
panic: 4
goroutine 1 [running]:
main.g(0x4)
/tmp/sandbox2114608904/prog.go:30 +0x1ec
main.g(0x3)
/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x2)
/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x1)
/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x0)
/tmp/sandbox2114608904/prog.go:34 +0x136
main.f()
/tmp/sandbox2114608904/prog.go:23 +0x7f
main.main()
/tmp/sandbox2114608904/prog.go:9 +0x3f
Program exited
从运行结果可以观察到defer的作用,即使方法g中当i为4时发生了panic,每个defer的函数调用依然正常被执行了,而且是先进后出的顺序被执行。就像是每次defer时,将被defer的函数调用push到一个栈数据结构中,当返回时,再从栈中挨个将defer的函数pop出来并执行。
recover函数调用必须使用defer关键字,就是因为defer的函数调用必然会被执行。可以将以上实例中defer recover部分打开观察输出,与第一节中defer recover输出类似,程序可以正常执行并正常退出。
我们再对源码做一下简单分析,以加深对panic及recover处理流程的理解。
首先简单了解下有关defer的一对方法:deferproc和deferreturn。
panic方法对应的实现为runtime.gopanic,recover方法对应的实现为runtime.gorecover。
源码如下(为了简化理解,省略了很多分支判断,只保留主流程的代码):
func gopanic(e interface{}) {
//获取当前goroutine的对象gp
gp := getg()
...
//将当前panic添加到gp的panic链表头部
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
//循环执行defer链表中的函数
for {
//获取gp的defer链表
d := gp._defer
if d == nil {
//如果没有defer,退出循环
break
}
...
done := true
...
//执行defer的函数调用
var regs abi.RegArgs
reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz), uint32(d.siz), ®s)
...
p.argp = nil
d._panic = nil
...
if done {
//清理defer对象,并设置下一个defer对象到gp的defer链表头部
d.fn = nil
gp._defer = d.link
freedefer(d)
}
if p.recovered {
//如果defer运行了recover函数,调用内置的recovery函数恢复调用
//recovery函数会将当前的调用栈改变到deferreturn,从而使得程序可以继续正常运行
...
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed") // mcall should not return
}
}
//如果没有recover,defer执行完毕,打印panic信息,并退出进程
preprintpanics(gp._panic)
fatalpanic(gp._panic) // should not return
*(*int)(nil) = 0 // not reached
}
//recover方法的实现
func gorecover(argp uintptr) interface{} {
gp := getg()
p := gp._panic
...
//recover方法仅有的一个作用,将recovered置为true
p.recovered = true
return p.arg
}
小结
最后我们通过一个简单的例子,看一下recover后如何打印panic信息,及如何阅读panic信息
示例是一个除零的panic:
package main
import (
"fmt"
"runtime"
)
func main() {
f()
}
func f() {
defer func() {
if r := recover(); r != nil {
printPanicInfo(r)
}
}()
g()
}
func g() {
a := 10
var b int
a = a / b
}
func printPanicInfo(r interface{}) {
buf := make([]byte, 64<<10)
buf = buf[:runtime.Stack(buf, false)]
s := fmt.Sprintf("%s\n%s", r, buf)
fmt.Println(s)
}
输出为:
//panic的原因
runtime error: integer divide by zero
//goroutine的id
goroutine 1 [running]:
//下面是runtime.Stack方法调用时的调用堆栈链,方法名称和方法被调用的文件行数成对出现
main.printPanicInfo(0x4b78c0, 0x572a10) //方法名称
E:/xxx/liuwei/test/main.go:29 +0x74 //方法所在的文件和行数
main.f.func1()
E:/xxx/liuwei/test/main.go:15 +0x59
panic(0x4b78c0, 0x572a10)
C:/go1.13/go/src/runtime/panic.go:679 +0x1c0 //panic被调用
main.g(...)
E:/xxx/liuwei/test/main.go:24 //发生panic的代码行数
main.f()
E:/xxx/liuwei/test/main.go:18 +0x50
main.main()
E:/xxx/liuwei/test/main.go:9 +0x27
打印的信息中主要由panic原因
和调用堆栈
组成,我们阅读堆栈信息时,可以首先找到runtime.panic
,它的下一条堆栈
记录就是发生panic的代码具体行数。然后再结合panic的原因信息,一般会很快了解到panic发生的原因。
另外除了panic之外还有一种fatalpanic,这种严重的异常无法使用recover恢复,一般是运行时检测到不可恢复的操作时抛出。例如发生map并发写时会throw(“concurrent map writes”),导致进程崩溃。
特别提示
参考资料
6. 深度细节 | Go 的 panic 秘密都在这
7. go panic 的实现原理