golang panic和recover 實現原理
這篇文章是系列文章中的第二篇,系列文章主要包括:
- golang defer的原理
- golang panic和recover()函數的原理(包括golang對於錯誤處理方式)
- defer性能損耗的討論以及最重要的應用場景
- defer在golang 1.13 上的性能
panic 能中斷一個程序的執行,同時也能在一定情況下進行恢復(recover)。我們就來看一看 panic 和 recover 這對關鍵字的實現機制。根據我們對 Go 的實踐,可以預見的是,他們的實現跟runtime調度器和 defer 關鍵字也緊密相關。
思考
1.爲什麼go 進程會終止
func main() {
panic("sim lou.")
}
輸出結果是:
panic: sim lou.
goroutine 1 [running]:
main.main()
/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test1.go:4 +0x39
Process finished with exit code 2
這裏思考一下,爲什麼執行 panic 後會導致應用程序運行中止?或者說執行panic爲什麼導致進程終止了?
2. 爲什麼不會中止運行
func main() {
defer func() {
if err := recover(); err != nil {
log.Printf("recover: %v", err)
}
}()
panic("sim lou.")
}
輸出結果是:
2019/10/26 22:19:33 recover: sim lou.
Process finished with exit code 0
思考一下爲什麼加上 defer + recover 組合就可以保護應用程序不會退出。
3.不設置 defer 行不
上面問題二是 defer + recover 組合,那我去掉 defer 是不是也可以呢?如下:
func main() {
if err := recover(); err != nil {
log.Printf("recover: %v", err)
}
panic("sim lou.")
}
運行結果:
panic: sim lou.
goroutine 1 [running]:
main.main()
/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test3.go:9 +0xa1
Process finished with exit code 2
不行!!!我們常說 defer + recover 組合 “萬能” 捕獲。但是爲什麼呢。去掉 defer 後爲什麼就無法捕獲了?
思考一下,爲什麼需要設置 defer 後 recover 才能起作用?
同時你還需要仔細想想,我們設置 defer + recover 組合後就能無憂無慮了嗎,各種 “亂” 寫了嗎?
4. 爲什麼起個 goroutine 就不行
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("recover: %v", err)
}
}()
}()
panic("qwertyuiop.")
}
輸出結果:
panic: qwertyuiop.
goroutine 1 [running]:
main.main()
/Users/ytlou/Desktop/golang/golang_study/study/basic/panic/panic_test4.go:13 +0x51
請思考一下,爲什麼新起了一個 Goroutine 就無法捕獲到異常了?到底發生了什麼事…
但是我們改一下:
func main() {
go func() {
defer func() {
if err := recover(); err != nil {
log.Printf("recover: %v", err)
}
}()
panic("aim lou 2.")
}()
time.Sleep(1* time.Second)
}
輸出是:
2019/10/26 22:27:50 recover: aim lou 2.
爲什麼我們把panic放到自定義的協程裏面recover又可以work了呢?
基於前面的四個問題,我們閱讀源碼,從源碼找到root cause.
數據結構
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg interface{} // argument to panic
link *_panic // link to earlier panic
recovered bool // whether this panic is over
aborted bool // the panic was aborted
}
在 panic 中是使用 _panic 作爲其基礎單元的,每執行一次 panic 語句,都會創建一個 _panic 對象。它包含了一些基礎的字段用於存儲當前的 panic 調用情況,涉及的字段如下
- argp:指向 defer 延遲調用的參數的指針
- arg:panic 的原因,也就是調用 panic 時傳入的參數
- link:指向上一個調用的 _panic,這裏說明panic也是一個鏈表
- recovered:panic 是否已經被處理過,也就是是否被 recover
- aborted:panic 是否被中止
通過查看 link 字段,可得知其是一個鏈表的數據結構,如下圖:
+-----------+ +-----------+ +-----------+
| _panic | +----> _panic | +-----> _panic |
+-----------+ | +-----------+ | +-----------+
| ...... | | | ...... | | | ...... |
+-----------+ | +-----------+ | +-----------+
| link |-----+ | link +----+ | link |
+-----------+ +-----------+ +-----------+
| ...... | | ...... | | ...... |
+-----------+ +-----------+ +-----------+
panic
我們先看看panic生成的彙編代碼:
func main() {
panic("sim lou.")
}
彙編代碼:
"".main STEXT size=65 args=0x0 locals=0x18
0x0000 00000 (panic_test1.go:3) TEXT "".main(SB), ABIInternal, $24-0
......
0x002f 00047 (panic_test1.go:4) PCDATA $0, $0
0x002f 00047 (panic_test1.go:4) MOVQ AX, 8(SP)
0x0034 00052 (panic_test1.go:4) CALL runtime.gopanic(SB)
.......
可以看到 panic 翻譯成彙編代碼主要是調用了 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
}
// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
// take defer off list. The earlier panic or Goexit will not continue running.
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
// Mark defer as started, but keep on list, so that traceback
// can find and update the defer's argument frame if stack growth
// or a garbage collection happens before reflectcall starts executing d.fn.
d.started = true
// Record the panic that is running the defer.
// If there is a new panic during the deferred call, that panic
// will find d in the list and will mark d._panic (this panic) aborted.
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
// reflectcall did not panic. Remove d.
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
//GC()
pc := d.pc
sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
freedefer(d)
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// Aborted panics are marked but remain on the g.panic list.
// Remove them from the list.
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // must be done with signal
gp.sig = 0
}
// Pass information about recovering frame to recovery.
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
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,並將這個panic頭插入當前goroutine的panic鏈表中。
- 獲取當前 Goroutine 上掛載的 _defer(數據結構也是鏈表)
- 若當前存在 defer 調用,則調用 reflectcall 方法去執行先前 defer 中延遲執行的代碼。reflectcall方法若在執行過程中需要運行 recover 將會調用 gorecover 方法。
- 結束前,使用 preprintpanics 方法打印出所涉及的 panic 消息
- 最後調用 fatalpanic 中止應用程序,實際是執行 exit(2) 進行最終退出行爲的。
通過對上述代碼的執行分析,可得知 panic 方法實際上就是處理當前 Goroutine(g) 上所掛載的 ._panic 鏈表(所以無法對其他 Goroutine 的異常事件響應),然後對其所屬的 defer 鏈表和 recover 進行檢測並處理,最後調用退出命令中止應用程序。
恢復 recover panic
func main() {
defer func() {
if err := recover(); err != nil {
log.Printf("recover: %v", err)
}
}()
panic("sim lou.")
}
輸出結果:
2019/10/27 12:39:30 recover: sim lou.
Process finished with exit code 0
我們看彙編代碼,panic是怎麼被recover的:
"".main STEXT size=118 args=0x0 locals=0x50
......
0x003a 00058 (panic_test2.go:6) CALL runtime.deferprocStack(SB)
......
0x005a 00090 (panic_test2.go:12) CALL runtime.gopanic(SB)
......
0x0060 00096 (panic_test2.go:6) CALL runtime.deferreturn(SB)
......
"".main.func1 STEXT size=151 args=0x0 locals=0x40
0x0000 00000 (panic_test2.go:6) TEXT "".main.func1(SB), ABIInternal, $64-0
......
0x0026 00038 (panic_test2.go:7) CALL runtime.gorecover(SB)
......
0x0092 00146 (panic_test2.go:6) JMP 0
通過分析底層調用,可得知主要是如下幾個方法:
- runtime.deferprocStack
- runtime.gopanic
- runtime.deferreturn
- runtime.gorecover
前面我們說了簡單的流程,gopanic 方法會遍歷調用當前 Goroutine 下的 defer 鏈表,若 reflectcall 執行中遇到 recover 就會調用 gorecover 進行處理,該方法代碼如下:
func gorecover(argp uintptr) interface{} {
// Must be in a function running as part of a deferred call during the panic.
// Must be called from the topmost function of the call
// (the function used in the defer statement).
// p.argp is the argument pointer of that topmost deferred function call.
// Compare against argp reported by caller.
// If they match, the caller is the one who can recover.
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) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
// d's arguments need to be in the stack.
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), "]\n")
throw("bad recovery")
}
// Make the deferproc for this d return again,
// this time returning 1. The calling function will
// jump to the standard return epilogue.
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 的流程了.
panic 拋出
當然如果所有的 defer 都沒有指明顯式的 recover,那麼這時候則直接在運行時拋出 panic 信息:
// 消耗完所有的 defer 調用,保守地進行 panic
// 因爲在凍結之後調用任意用戶代碼是不安全的,所以我們調用 preprintpanics 來調用
// 所有必要的 Error 和 String 方法來在 startpanic 之前準備 panic 字符串。
preprintpanics(gp._panic)
fatalpanic(gp._panic) // 不應該返回
*(*int)(nil) = 0 // 無法觸及
總結:
從 panic 和 recover 這對關鍵字的實現上可以看出,可恢復的 panic 必須要 recover 的配合。 而且,這個 recover 必須位於同一 goroutine 的直接調用鏈上(例如,如果 A 依次調用了 B 和 C,而 B 包含了 recover,而 C 發生了 panic,則這時 B 的 panic 無法恢復 C 的 panic; 又例如 A 調用了 B 而 B 又調用了 C,那麼 C 發生 panic 時,如果 A 要求了 recover 則仍然可以恢復), 否則無法對 panic 進行恢復。
當一個 panic 被恢復後,調度並因此中斷,會重新進入調度循環,進而繼續執行 recover 後面的代碼, 包括比 recover 更早的 defer(因爲已經執行過得 defer 已經被釋放,而尚未執行的 defer 仍在 goroutine 的 defer 鏈表中), 或者 recover 所在函數的調用方。