做网站上传服务器,wordpress安全优化教程,株洲网站建设报价,生成器一、前言
我们的应用程序常常会出现异常#xff0c;包括由运行时检测到的异常或者应用开发者自己抛出的异常。
异常在一些其他语言中#xff0c;如c、java#xff0c;被叫做Exception#xff0c;主要由抛出异常和捕获异常两部分组成。异常在go语言中#xff0c;叫做pani…一、前言
我们的应用程序常常会出现异常包括由运行时检测到的异常或者应用开发者自己抛出的异常。
异常在一些其他语言中如c、java被叫做Exception主要由抛出异常和捕获异常两部分组成。异常在go语言中叫做panic且由panic和recover方法组成panic用来抛出recover用来从panic中恢复。
1.1 panic实例分析
以下是一段简单的panic和recover使用示例
package mainimport fmtfunc 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: 0goroutine 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 0x19Program 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之后的代码没有执行。
1.2 官方翻译
panic是go的内置函数它可以终止程序的正常执行流程并发出panic。
比如当函数F调用panicF的执行将被终止并返回到调用者。对调用者而言F就像调用者直接调用了panic。该过程一直跟随堆栈向上直到当前goroutine中的所有函数都返回此时 程序崩溃 panic可以通过直接调用panic产生。同时也可能由运行时的错误所产生例如数组越界访问。
recover是go语言的内置函数它的唯一作用是可以从panic中重新控制goroutine的执行。recover必须通过defer来运行。
在正常的执行流程中调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panicrecover将会捕获到panic抛出的信息同时恢复其正常的执行流程。 小结 panic可以令程序崩溃异常退出recover可以让程序从panic中恢复并正常运行即使单个goroutine中发生了panic也会使整个进程崩溃recover必须通过defer来运行
二、实现原理
2.1 panic从哪来
我们可以手动调用内置函数panic但是那些空指针、数组越界等运行时panic是如何被检测到的下面针对这一问题做一些代码调试
2.1.1 常见的几种panic
空指针 invalid memory address or nil pointer dereference数组越界 index out of rangeslice bounds out of range除数为零 integer divide by zero自定义panic
2.1.2 追踪panic来源
测试代码
package main
func main() {a : 0testDivide(a) //除零//testOutRange() //越界//testNil() //空指针//panic(666) //自定义panic
}
func testDivide(a int) {b : 10 / a_ b
}
func testOutRange() {var a []inta[0] 2
}
func testNil() {var a *int*a 1
}调试代码
与linux平台下的gdb调试工具类似dlv用来调试go语言编写的程序。
dlv是一个命令行工具它包含了多个调试命令例如运行程序、下断点、打印变量、step in、step out等。我们常用的go语言编辑器如vscode、golang等的可视化调试也是调用dlv。
找出panic是怎么产生的
这里我们先给出结论具体调试过程产生的代码请往下看
调试自定义panic方法
在8行处下断点打印main方法的汇编代码可以看到panic方法编译后实质是runtime包中的gopanic方法
使用dlv调试testDivide中的代码有以下几个关键步骤
在12行处下断点打印testDivide方法的汇编代码testDivide方法中测试参数a的值是否为零如果为零则调用runtime包的panicdivide方法调用runtime包的panicdivide方法panicdivide方法调用了panic打印panicdivide的汇编代码panic方法编译后实质是runtime包中的gopanic方法
所以其实panic方法实际调用了runtime.gopanic
编译后的testDivide方法中除了正常的除法逻辑编译器塞入了判断除数是否为零的代码分支当除数为零则进入panic流程与自定义panic相同同样调用了runtime.gopanic其他数组越界及空指针也都是调用了runtime.gopanic进入panic流程不同的是数组越界与除数为零相似是通过编译器塞入判断分支进行越界检测而空指针是通过访问非法地址产生中断进入panic流程。 小结 panic可以由开发者调用内置函数抛出编译器将检测异常的代码加入到程序中会出现异常时抛出某些非法指令产生中断并由中断处理函数抛出
2.2 panic到哪去
2.2.1 panic后的处理流程
由于panic和defer有着难解难分的关系我们先了解一下defer。
defer定义的官翻
defer语句将函数调用保存到一个列表上。保存的调用列表在当前函数返回前执行。Defer通常用于简化执行各种清理操作的函数。
通俗地说就是defer保证函数调用不管在什么情况下即使当前函数发生panic在当前函数返回前必然执行。另外defer的函数调用符合先进后出的规则即先defer的函数后执行。
我们看一个示例程序它是第一节示例程序的升级版本方法g中会调用自身
package mainimport fmtfunc 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)
}程序运行结果如下
作者刘玮
链接https://www.zhihu.com/question/295517993/answer/2421882834
来源知乎
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。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: 4goroutine 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 0x3fProgram exited从运行结果可以观察到defer的作用即使方法g中当i为4时发生了panic每个defer的函数调用依然正常被执行了而且是先进后出的顺序被执行。就像是每次defer时将被defer的函数调用push到一个栈数据结构中当返回时再从栈中挨个将defer的函数pop出来并执行。
recover函数调用必须使用defer关键字就是因为defer的函数调用必然会被执行。可以将以上实例中defer recover部分打开观察输出与第一节中defer recover输出类似程序可以正常执行并正常退出。
2.2.2 源码分析
我们再对源码做一下简单分析以加深对panic及recover处理流程的理解。
首先简单了解下有关defer的一对方法deferproc和deferreturn。
deferproc即defer关键字的实现它将defer的函数调用push到当前goroutine中的defer链表头部deferreturn当一个函数中包含defer操作编译器将在函数返回前插入一条deferreturn调用deferreturn会将当前函数中defer的函数调用依次执行完毕
panic方法对应的实现为runtime.gopanicrecover方法对应的实现为runtime.gorecover。
源码如下为了简化理解省略了很多分支判断只保留主流程的代码
func gopanic(e interface{}) {//获取当前goroutine的对象gpgp : getg()...//将当前panic添加到gp的panic链表头部var p _panicp.arg ep.link gp._panicgp._panic (*_panic)(noescape(unsafe.Pointer(p)))...//循环执行defer链表中的函数for {//获取gp的defer链表d : gp._deferif d nil {//如果没有defer退出循环break}...done : true...//执行defer的函数调用var regs abi.RegArgsreflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz), uint32(d.siz), regs)...p.argp nild._panic nil...if done {//清理defer对象并设置下一个defer对象到gp的defer链表头部d.fn nilgp._defer d.linkfreedefer(d)}if p.recovered {//如果defer运行了recover函数调用内置的recovery函数恢复调用//recovery函数会将当前的调用栈改变到deferreturn从而使得程序可以继续正常运行...gp.sigcode0 uintptr(sp)gp.sigcode1 pcmcall(recovery)throw(recovery failed) // mcall should not return}}//如果没有recoverdefer执行完毕打印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置为truep.recovered truereturn p.arg
}小结 panic处理过程中会检测是否有defer的函数调用如果有按照先进后出的顺序依次执行如果defer中有recover调用则将调用栈修改到deferreturn使得程序正常执行否则当defer的函数调用执行完后打印panic信息进程退出
2.3 panic 打印信息
最后我们通过一个简单的例子看一下recover后如何打印panic信息及如何阅读panic信息
示例是一个除零的panic
recover后调用printPanicInfo方法printPanicInfo使用runtime.Stack方法收集调用堆栈信息r为recover返回的参数即panic传入的参数一般为panic的具体原因本示例为runtime error: integer divide by zero将panic原因和堆栈信息拼接并打印
package main
import (fmtruntime
)
func main() {f()
}
func f() {defer func() {if r : recover(); r ! nil {printPanicInfo(r)}}()g()
}
func g() {a : 10var b inta a / b
}
func printPanicInfo(r interface{}) {buf : make([]byte, 6410)buf buf[:runtime.Stack(buf, false)]s : fmt.Sprintf(%s\n%s, r, buf)fmt.Println(s)
}
输出为
作者刘玮
链接https://www.zhihu.com/question/295517993/answer/2421882834
来源知乎
著作权归作者所有。商业转载请联系作者获得授权非商业转载请注明出处。//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”)导致进程崩溃。 特别提示 因为Golang的gorotuine机制panic在不同的gorotuine里面是单独的并不是整体处理。可能一个地方挂了就会整体挂掉这个要非常小心。
三、总结
panic() 会退出进程是因为调用了 exit 的系统调用recover() 并不是说只能在 defer 里面调用而是只能在 defer 函数中才能生效只有在 defer 函数里面才有可能遇到 _panic 结构recover() 所在的 defer 函数必须和 panic 都是挂在同一个 goroutine 上不能跨协程因为 gopanic 只会执行当前 goroutine 的延迟函数panic 的恢复就是重置 pc 寄存器直接跳转程序执行的指令跳转到原本 defer 函数执行完该跳转的位置deferreturn 执行从 gopanic 函数中跳出不再回来自然就不会再 fatalpanicpanic 为啥能嵌套这个问题就像是在问为什么函数调用可以嵌套一样因为这个本质是一样的。
参考资料 6. 深度细节 | Go 的 panic 秘密都在这 7. go panic 的实现原理