当前位置: 首页 > 后端技术 > PHP

深入理解Gopanic和recover

时间:2023-03-30 01:05:58 PHP

作为gophper,panic和recover相信大家并不陌生,但是大家有没有想过呢?我们执行完这两条语句之后。下面发生了什么?前几天刚和同事聊了相关的话题,发现大家对这方面的认识还比较模糊。希望这篇文章能从更深入的角度告诉你为什么,它到底有什么作用?原文地址:深入理解Gopanic和recover思考1.为什么停止运行funcmain(){panic("EDDYCJY.")}输出结果:$gorunmain.gopanic:EDDYCJY.goroutine1[running]:main.main()/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4+0x39exitstatus2请思考一下,为什么执行panic会导致应用程序停止运行?(而不是只说执行了panic,所以就结束了那么模棱两可)其次,为什么funcmain(){deferfunc(){iferr:=recover();err!=nil{log.Printf("recover:%v",err)}}()panic("EDDYCJY.")}输出结果:$gorunmain.go2019/05/1123:39:47recover:EDDYCJY。请想一想为什么你加上defer+recover的组合可以保护应用程序?3、不设置defer是否有效?第二个问题是defer+recover的组合。如果我删除延迟可以吗?如下:funcmain(){iferr:=recover();err!=nil{log.Printf("recover:%v",err)}panic("EDDYCJY.")}输出结果:$gorunmain.gopanic:EDDYCJY.goroutine1[running]:main.main()/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10+0xa1exitstatus2不行啊,毕竟,入门教程写的defer+recover组合“万能”"capturebutwhy。为什么去掉defer后就不能capture了呢?请大家想一想,为什么要设置defer之后recover才能起作用呢?同时也要仔细想想,设置defer之后我们就可以高枕无忧了+recover组合你写过各种“乱七八糟”的东西吗?4.为什么不能启动goroutinefuncmain(){gofunc(){deferfunc(){iferr:=recover();err!=nil{log.Printf("recover:%v",err)}}()}()panic("EDDYCJY.")}输出结果:$gorunmain.gopanic:EDDYCJY.goroutine1[running]:main.main()/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14+0x51exitstatus2请思考一下,为什么新的Goroutine不能捕获到异常?发生了什么...Sourcecode接下来我们就带着上面的4+1个思考小题开始分析分析源码,尝试从阅读源码中找到思考题的答案和更多why数据结构类型_panicstruct{argpunsafe.指针参数接口{}link*_panicrecoveredboolabortedbool}在panic中,_panic作为其基本单位。每次执行panic语句时,都会创建一个_panic。包含一些基本字段,用于存储当前的paniccall,涉及的字段如下:argp:指向deferdelaycall的参数的指针arg:panic的原因,即调用panic时传入的参数link:指向up_panicrecoveredofacall:panic是否被处理,即是否被recoveredaborted:panic是否被aborted另外通过查看link字段可以知道是一个链表数据结构,如如下图所示:panicpanicfuncmain(){panic("EDDYCJY.")}output:$gorunmain.gopanic:EDDYCJY.goroutine1[running]:main.main()/Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4+0x39exitstatus2我们回头看看panic处理具体逻辑的地方,如下:$gotoolcompile-Smain.go"".mainSTEXTsize=66args=0x0locals=0x180x000000000(main.go:23)TEXT"".main(SB),ABIInternal,$24-00x000000000(main.go:23)MOVQ(TLS),CX0x000900009(main.go:23)CMPQSP,16(CX)..0x002f00047(main.go:24)PCDATA$2,$00x002f00047(main.go:24)MOVQAX,8(SP)0x003400052(main.go:24)CALLruntime.gopanic(SB)显然汇编代码直接引用了runtime.gopanic的内部实现,我们来看看这个方法做了什么,如下(部分省略):funcgopanic(einterface{}){gp:=getg()...变量p_panicp.arg=ep.link=gp._panicgp._panic=(*_panic)(noescape(unsafe.Pointer(&p)))for{d:=gp._deferifd==nil{break}//推迟......d._panic=(*_panic)(noescape(unsafe.Pointer(&p)))p.argp=unsafe.Pointer(getargp(0))reflectcall(nil,unsafe.Pointer(d.fn),deferArgs(d),uint32(d.siz),uint32(d.siz))p.argp=nil//恢复...ifp.recovered{...mcall(recovery)throw("恢复失败")//mcall不应返回}}preprintpanics(gp._panic)fatalpanic(gp._panic)//不应返回*(*int)(nil)=0//未到达}获取指向当前Goroutine初始化的指针apanic的基本单元_panic,用于后续操作获取挂载在当前Goroutine上的_defer(数据结构也是链表)。如果当前有defer调用,则调用reflectcall方法执行上一次defer延迟的代码。需要运行recover会在结束前调用gorecover方法,使用preprintpanics方法打印出涉及的panic信息,最后调用fatalpanic终止应用,实际执行exit(2)执行最后的退出行为通过分析上面代码的执行我们可以知道panic方法实际上是在处理挂载在当前Goroutine(g)上的._panic链表(所以无法响应异常eventsofotherGoroutines),然后检测并处理defer链表并恢复所属,最后调用exit命令中止应用无法恢复的panicfatalpanicfuncfatalpanic(msgs*_panic){pc:=getcallerpc()sp:=getcallersp()gp:=getg()vardocrashboolsystemstack(func(){ifstartpanic_m()&&msgs!=nil{...printpanics(msgs)}docrash=dopanic_m(gp,pc,sp)})systemstack(func(){exit(2)})*(*int)(nil)=0}我们看到这个方法会在异常处理结束时执行,似乎它承担了所有的收尾工作。其实它在最后对程序执行exit指令终止运行,只是在结束前通过printpanics递归输出了所有的异常信息和参数。代码如下:funcprintpanics(p*_panic){ifp.link!=nil{printpanics(p.link)print("\t")}print("panic:")printany(p.arg)ifp.recovered{print("[recovered]")}print("\n")}所以不要以为所有的异常都能恢复。其实fatalerror和runtime.throw是无法恢复的,就算是oom也是直接终止程序,有反手给你exit(2)教你做人。所以写代码的时候要相对小心,“panic”是不可恢复场景的恢复recoverfuncmain(){deferfunc(){iferr:=recover();err!=nil{log.Printf("recover:%v",err)}}()panic("EDDYCJY.")}输出:$gorunmain.go2019/05/1123:39:47恢复:EDDYCJY。符合预期,成功捕获到异常。但是恢复如何恢复恐慌呢?再看汇编代码,如下:$gotoolcompile-Smain.go"".mainSTEXTsize=110args=0x0locals=0x180x000000000(main.go:5)TEXT"".main(SB),ABIInternal,$24-0...0x002400036(main.go:6)LEAQ"".main.func1f(SB),AX0x002b00043(main.go:6)PCDATA$2,$00x002b00043(main.go:6)MOVQAX,8(SP)0x003000048(main.go:6)CALLruntime.deferproc(SB)...0x005000080(main.go:12)CALLruntime.gopanic(SB)0x005500085(main.去:12)UNDEF0x005700087(main.go:6)XCHGLAX,AX0x005800088(main.go:6)CALLruntime.deferreturn(SB)...0x002200034(main.go:7)MOVQAX,(SP)0x002600038(main.go:7)CALLruntime.gorecover(SB)0x002b00043(main.go:7)PCDATA$2,$10x002b00043(main.go:7)MOVQ16(SP),AX0x003000048(main.go:7)MOVQ8(SP),CX...0x005600086(main.go:8)LEAQgo.string."recover:%v"(SB),AX...0x008600134(main.go:8)CALLlog.Printf(SB)...通过分析底层调用可以知道主要有以下几个方法:runtime.deferprocruntime.gopanicruntime.deferreturnruntime.gorecoverin上一节中,我们描述了简单的过程。gopanic方法会调用当前Goroutine下的defer列表。如果在reflectcall执行过程中遇到recover,会调用gorecover进行处理。该方法的代码如下:funcgorecover(argpuintptr)interface{}{gp:=getg()p:=gp._panicifp!=nil&&!p.recovered&&argp==uintptr(p.argp){p.recovered=truereturnp.arg}returnnil}这段代码,看起来很简单,核心是修改recovered字段,用于标识当前panic是否已经被recover处理过。但这和我们想象的不一样。程序是怎么从panic中流回来的?它是在核心方法中处理的吗?我们再看一下gopanic的代码,如下:funcgopanic(einterface{}){...for{//defer......pc:=d.pcsp:=unsafe.Pointer(d.sp)//必须是指针,以便它在堆栈复制期间得到调整freedefer(d)//恢复...ifp.recovered{atomic.Xadd(&runningPanicDefers,-1)gp._panic=p.linkforgp._panic!=nil&&gp._panic.aborted{gp._panic=gp._panic.link}如果gp._panic==nil{gp.sig=0}gp.sigcode0=uintptr(sp)gp.sigcode1=pcmcall(恢复)throw("recoveryfailed")}}...}我们回到gopanic方法仔细看一下,发现里面其实包含了recoveryflow的处理代码。恢复过程如下:判断当前_panic中的recover是否已经标记为processing。从_pa??nic列表中删除已经标记为suspended的panic事件,即删除已经恢复的panic事件,将需要恢复的相关栈帧信息传递给recovery方法。gp参数(每个堆栈帧对应一个未完成的函数。函数的返回地址和局部变量存放在栈帧中)Executerecovery执行恢复动作从流程上看,核心是recovery方法。承担异常流量控制的责任。代码如下:funcrecovery(gp*g){sp:=gp.sigcode0pc:=gp.sigcode1ifsp!=0&&(sp(SB)MOVWgobuf_sp(R1),R13//恢复SP==R13MOVWgobuf_lr(R1),LRMOVWgobuf_ret(R1),R0MOVWgobuf_ctxt(R1),R7MOVW$0,R11MOVWR11,gobuf_sp(R1)//清除以帮助垃圾收集器MOVWR11,gobuf_ret(R1)MOVWR11,gobuf_lr(R1)xtgobuf_ct(R1)MOVWgobuf_pc(R1),R11CMPR11,R11//设置==测试的条件码,栈拆分B需要(R11)看代码可以知道它的主要作用是从Gobuf恢复状态。简单来说就是将寄存器的值修改为Goroutine(g)对应的值,Gobuf在文章中多次提到,如下:typegobufstruct{spuintptrpcuintptrgguintptrctxtunsafe.Pointerretsys.Uintreglruintptrbpuintptr}是有道理的。实际上,它存储了一些Goroutine在切换上下文时需要的东西。Expandconst(OPANIC//panic(Left)ORECOVER//recover()...)...funcwalkexpr(n*Node,init*Nodes)*Node{...switchn.Op{默认值:Dump("walk",n)Fatalf("walkexpr:switch1unknownop%+S",n)caseONONAME,OINDREGSP,OEMPTY,OGETG:caseOTYPE,ONAME,OLITERAL:...案例OPANIC:n=mkcall("gopanic",nil,init,n.Left)caseORECOVER:n=mkcall("gorecover",n.Type,init,nod(OADDR,nodfp,nil))...}其实调用panic和recover关键字时,它们首先在编译阶段转换为相应的操作码,然后由编译器转换为相应的运行时方法。并不是想象中的一步到位。有兴趣的朋友可以研究总结。本文主要深入分析panic和recover关键字的源码。开头的4+1思维题,就是希望大家能有问题。要学,事半功倍另外这篇文章和defer有一定的相关性,所以需要一定的基础知识。如果刚才看这部分没看懂,可以学习后再看一遍,加深印象。最后,你现在能回答这些思考题了吗?你一说,你就真的明白了:)