摘要:恢復流程如下判斷當前中的是否已標注為處理從鏈表中刪除已標注中止的事件,也就是刪除已經被恢復的事件將相關需要恢復的棧幀信息傳遞給方法的參數每個棧幀對應著一個未運行完的函數。
作為一個 gophper,我相信你對于 panic 和 recover 肯定不陌生,但是你有沒有想過。當我們執行了這兩條語句之后。底層到底發生了什么事呢?前幾天和同事剛好聊到相關的話題,發現其實大家對這塊理解還是比較模糊的。希望這篇文章能夠從更深入的角度告訴你為什么,它到底做了什么事?
原文地址:深入理解 Go panic and recover
思考 一、為什么會中止運行func main() { panic("EDDYCJY.") }
輸出結果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39 exit status 2
請思考一下,為什么執行 panic 后會導致應用程序運行中止?(而不是單單說執行了 panic 所以就結束了這么含糊)
二、為什么不會中止運行func main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("EDDYCJY.") }
輸出結果:
$ go run main.go 2019/05/11 23:39:47 recover: EDDYCJY.
請思考一下,為什么加上 defer + recover 組合就可以保護應用程序?
三、不設置 defer 行不上面問題二是 defer + recover 組合,那我去掉 defer 是不是也可以呢?如下:
func main() { if err := recover(); err != nil { log.Printf("recover: %v", err) } panic("EDDYCJY.") }
輸出結果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:10 +0xa1 exit status 2
竟然不行,啊呀畢竟入門教程都寫的 defer + recover 組合 “萬能” 捕獲。但是為什么呢。去掉 defer 后為什么就無法捕獲了?
請思考一下,為什么需要設置 defer 后 recover 才能起作用?
同時你還需要仔細想想,我們設置 defer + recover 組合后就能無憂無慮了嗎,各種 “亂” 寫了嗎?
四、為什么起個 goroutine 就不行func main() { go func() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() }() panic("EDDYCJY.") }
輸出結果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:14 +0x51 exit status 2
請思考一下,為什么新起了一個 Goroutine 就無法捕獲到異常了?到底發生了什么事...
源碼接下來我們將帶著上述 4+1 個小思考題,開始對源碼的剖析和分析,嘗試從閱讀源碼中找到思考題的答案和更多為什么
數據結構type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool }
在 panic 中是使用 _panic 作為其基礎單元的,每執行一次 panic 語句,都會創建一個 _panic。它包含了一些基礎的字段用于存儲當前的 panic 調用情況,涉及的字段如下:
argp:指向 defer 延遲調用的參數的指針
arg:panic 的原因,也就是調用 panic 時傳入的參數
link:指向上一個調用的 _panic
recovered:panic 是否已經被處理,也就是是否被 recover
aborted:panic 是否被中止
另外通過查看 link 字段,可得知其是一個鏈表的數據結構,如下圖:
恐慌 panicfunc main() { panic("EDDYCJY.") }
輸出結果:
$ go run main.go panic: EDDYCJY. goroutine 1 [running]: main.main() /Users/eddycjy/go/src/github.com/EDDYCJY/awesomeProject/main.go:4 +0x39 exit status 2
我們去反查一下 panic 處理具體邏輯的地方在哪,如下:
$ go tool compile -S main.go "".main STEXT size=66 args=0x0 locals=0x18 0x0000 00000 (main.go:23) TEXT "".main(SB), ABIInternal, $24-0 0x0000 00000 (main.go:23) MOVQ (TLS), CX 0x0009 00009 (main.go:23) CMPQ SP, 16(CX) ... 0x002f 00047 (main.go:24) PCDATA $2, $0 0x002f 00047 (main.go:24) MOVQ AX, 8(SP) 0x0034 00052 (main.go:24) CALL runtime.gopanic(SB)
顯然匯編代碼直指內部實現是 runtime.gopanic,我們一起來看看這個方法做了什么事,如下(省略了部分):
func gopanic(e interface{}) { gp := getg() ... var p _panic p.arg = e p.link = gp._panic gp._panic = (*_panic)(noescape(unsafe.Pointer(&p))) for { d := gp._defer if d == nil { break } // defer... ... 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 // recover... if p.recovered { ... mcall(recovery) throw("recovery failed") // mcall should not return } } preprintpanics(gp._panic) fatalpanic(gp._panic) // should not return *(*int)(nil) = 0 // not reached }
獲取指向當前 Goroutine 的指針
初始化一個 panic 的基本單位 _panic 用作后續的操作
獲取當前 Goroutine 上掛載的 _defer(數據結構也是鏈表)
若當前存在 defer 調用,則調用 reflectcall 方法去執行先前 defer 中延遲執行的代碼,若在執行過程中需要運行 recover 將會調用 gorecover 方法
結束前,使用 preprintpanics 方法打印出所涉及的 panic 消息
最后調用 fatalpanic 中止應用程序,實際是執行 exit(2) 進行最終退出行為的
通過對上述代碼的執行分析,可得知 panic 方法實際上就是處理當前 Goroutine(g) 上所掛載的 ._panic 鏈表(所以無法對其他 Goroutine 的異常事件響應),然后對其所屬的 defer 鏈表和 recover 進行檢測并處理,最后調用退出命令中止應用程序
無法恢復的恐慌 fatalpanicfunc fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() var docrash bool systemstack(func() { if startpanic_m() && msgs != nil { ... printpanics(msgs) } docrash = dopanic_m(gp, pc, sp) }) systemstack(func() { exit(2) }) *(*int)(nil) = 0 }
我們看到在異常處理的最后會執行該方法,似乎它承擔了所有收尾工作。實際呢,它是在最后對程序執行 exit 指令來達到中止運行的作用,但在結束前它會通過 printpanics 遞歸輸出所有的異常消息及參數。代碼如下:
func printpanics(p *_panic) { if p.link != nil { printpanics(p.link) print(" ") } print("panic: ") printany(p.arg) if p.recovered { print(" [recovered]") } print(" ") }
所以不要以為所有的異常都能夠被 recover 到,實際上像 fatal error 和 runtime.throw 都是無法被 recover 到的,甚至是 oom 也是直接中止程序的,也有反手就給你來個 exit(2) 教做人。因此在寫代碼時你應該要相對注意些,“恐慌” 是存在無法恢復的場景的
恢復 recoverfunc main() { defer func() { if err := recover(); err != nil { log.Printf("recover: %v", err) } }() panic("EDDYCJY.") }
輸出結果:
$ go run main.go 2019/05/11 23:39:47 recover: EDDYCJY.
和預期一致,成功捕獲到了異常。但是 recover 是怎么恢復 panic 的呢?再看看匯編代碼,如下:
$ go tool compile -S main.go "".main STEXT size=110 args=0x0 locals=0x18 0x0000 00000 (main.go:5) TEXT "".main(SB), ABIInternal, $24-0 ... 0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX 0x002b 00043 (main.go:6) PCDATA $2, $0 0x002b 00043 (main.go:6) MOVQ AX, 8(SP) 0x0030 00048 (main.go:6) CALL runtime.deferproc(SB) ... 0x0050 00080 (main.go:12) CALL runtime.gopanic(SB) 0x0055 00085 (main.go:12) UNDEF 0x0057 00087 (main.go:6) XCHGL AX, AX 0x0058 00088 (main.go:6) CALL runtime.deferreturn(SB) ... 0x0022 00034 (main.go:7) MOVQ AX, (SP) 0x0026 00038 (main.go:7) CALL runtime.gorecover(SB) 0x002b 00043 (main.go:7) PCDATA $2, $1 0x002b 00043 (main.go:7) MOVQ 16(SP), AX 0x0030 00048 (main.go:7) MOVQ 8(SP), CX ... 0x0056 00086 (main.go:8) LEAQ go.string."recover: %v"(SB), AX ... 0x0086 00134 (main.go:8) CALL log.Printf(SB) ...
通過分析底層調用,可得知主要是如下幾個方法:
runtime.deferproc
runtime.gopanic
runtime.deferreturn
runtime.gorecover
在上小節中,我們講述了簡單的流程,gopanic 方法會調用當前 Goroutine 下的 defer 鏈表,若 reflectcall 執行中遇到 recover 就會調用 gorecover 進行處理,該方法代碼如下:
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
這代碼,看上去挺簡單的,核心就是修改 recovered 字段。該字段是用于標識當前 panic 是否已經被 recover 處理。但是這和我們想象的并不一樣啊,程序是怎么從 panic 流轉回去的呢?是不是在核心方法里處理了呢?我們再看看 gopanic 的代碼,如下:
func gopanic(e interface{}) { ... for { // defer... ... pc := d.pc sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy freedefer(d) // recover... if p.recovered { atomic.Xadd(&runningPanicDefers, -1) gp._panic = p.link for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } if gp._panic == nil { gp.sig = 0 } gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw("recovery failed") } } ... }
我們回到 gopanic 方法中再仔細看看,發現實際上是包含對 recover 流轉的處理代碼的。恢復流程如下:
判斷當前 _panic 中的 recover 是否已標注為處理
從 _panic 鏈表中刪除已標注中止的 panic 事件,也就是刪除已經被恢復的 panic 事件
將相關需要恢復的棧幀信息傳遞給 recovery 方法的 gp 參數(每個棧幀對應著一個未運行完的函數。棧幀中保存了該函數的返回地址和局部變量)
執行 recovery 進行恢復動作
從流程來看,最核心的是 recovery 方法。它承擔了異常流轉控制的職責。代碼如下:
func recovery(gp *g) { sp := gp.sigcode0 pc := gp.sigcode1 if sp != 0 && (sp < gp.stack.lo || gp.stack.hi < sp) { print("recover: ", hex(sp), " not in [", hex(gp.stack.lo), ", ", hex(gp.stack.hi), "] ") throw("bad recovery") } gp.sched.sp = sp gp.sched.pc = pc gp.sched.lr = 0 gp.sched.ret = 1 gogo(&gp.sched) }
粗略一看,似乎就是很簡單的設置了一些值?但實際上設置的是編譯器中偽寄存器的值,常常被用于維護上下文等。在這里我們需要結合 gopanic 方法一同觀察 recovery 方法。它所使用的棧指針 sp 和程序計數器 pc 是由當前 defer 在調用流程中的 deferproc 傳遞下來的,因此實際上最后是通過 gogo 方法跳回了 deferproc 方法。另外我們注意到:
gp.sched.ret = 1
在底層中程序將 gp.sched.ret 設置為了 1,也就是沒有實際調用 deferproc 方法,直接修改了其返回值。意味著默認它已經處理完成。直接轉移到 deferproc 方法的下一條指令去。至此為止,異常狀態的流轉控制就已經結束了。接下來就是繼續走 defer 的流程了
為了驗證這個想法,我們可以看一下核心的跳轉方法 gogo ,代碼如下:
// void gogo(Gobuf*) // restore state from Gobuf; longjmp TEXT runtime·gogo(SB),NOSPLIT,$8-4 MOVW buf+0(FP), R1 MOVW gobuf_g(R1), R0 BL setg<>(SB) MOVW gobuf_sp(R1), R13 // restore SP==R13 MOVW gobuf_lr(R1), LR MOVW gobuf_ret(R1), R0 MOVW gobuf_ctxt(R1), R7 MOVW $0, R11 MOVW R11, gobuf_sp(R1) // clear to help garbage collector MOVW R11, gobuf_ret(R1) MOVW R11, gobuf_lr(R1) MOVW R11, gobuf_ctxt(R1) MOVW gobuf_pc(R1), R11 CMP R11, R11 // set condition codes for == test, needed by stack split B (R11)
通過查看代碼可得知其主要作用是從 Gobuf 恢復狀態。簡單來講就是將寄存器的值修改為對應 Goroutine(g) 的值,而在文中講了很多次的 Gobuf,如下:
type gobuf struct { sp uintptr pc uintptr g guintptr ctxt unsafe.Pointer ret sys.Uintreg lr uintptr bp uintptr }
講道理,其實它存儲的就是 Goroutine 切換上下文時所需要的一些東西
拓展const( OPANIC // panic(Left) ORECOVER // recover() ... ) ... func walkexpr(n *Node, init *Nodes) *Node { ... switch n.Op { default: Dump("walk", n) Fatalf("walkexpr: switch 1 unknown op %+S", n) case ONONAME, OINDREGSP, OEMPTY, OGETG: case OTYPE, ONAME, OLITERAL: ... case OPANIC: n = mkcall("gopanic", nil, init, n.Left) case ORECOVER: n = mkcall("gorecover", n.Type, init, nod(OADDR, nodfp, nil)) ... }
實際上在調用 panic 和 recover 關鍵字時,是在編譯階段先轉換為相應的 OPCODE 后,再由編譯器轉換為對應的運行時方法。并不是你所想像那樣一步到位,有興趣的小伙伴可以研究一下
總結本文主要針對 panic 和 recover 關鍵字進行了深入源碼的剖析,而開頭的 4+1 個思考題,就是希望您能夠帶著疑問去學習,達到事半功倍的功效
另外本文和 defer 有一定的關聯性,因此需要有一定的基礎知識。若剛剛看的時候這部分不理解,學習后可以再讀一遍加深印象
在最后,現在的你可以回答這幾個思考題了嗎?說出來了才是真的懂 :)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31533.html
摘要:在上一章節深入理解中,我們發現了與其關聯性極大,還是覺得非常有必要深入一下。而返回的就是,因此可以防止重復調用小結在這個函數中會為新的設置一些基礎屬性,并將調用函數的參數集傳入。 在上一章節 《深入理解 Go panic and recover》 中,我們發現了 defer 與其關聯性極大,還是覺得非常有必要深入一下。希望通過本章節大家可以對 defer 關鍵字有一個深刻的理解,那么我...
摘要:還有一種情況就是當你在一行中寫了多個語句,也需要使用分號來分開由于語言詞法分析器添加分號的特殊性,所以在有些情況下需要注意你都不應該將一個控制結構或的左大括號放在下一行。 Go語言中變量的聲明和JavaScript很像,使用var關鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個變量 var m int = 10 // 聲明初始化多個變量 var i, j...
摘要:還有一種情況就是當你在一行中寫了多個語句,也需要使用分號來分開由于語言詞法分析器添加分號的特殊性,所以在有些情況下需要注意你都不應該將一個控制結構或的左大括號放在下一行。 Go語言中變量的聲明和JavaScript很像,使用var關鍵字,變量的聲明、定義有好幾種形式 1. 變量和常量 // 聲明并初始化一個變量 var m int = 10 // 聲明初始化多個變量 var i, j...
閱讀 3577·2021-11-24 10:19
閱讀 3710·2021-09-30 09:47
閱讀 1282·2019-08-30 15:56
閱讀 780·2019-08-29 15:11
閱讀 893·2019-08-29 13:43
閱讀 3557·2019-08-28 18:25
閱讀 2149·2019-08-26 13:27
閱讀 1427·2019-08-26 11:44