# 3.5 恐慌與恢復內建函數
panic 能中斷一個程序的執行,同時也能在一定情況下進行恢復。本節我們就來看一看 panic 和 recover 這對關鍵字 的實現機制。根據我們對 Go 的實踐,可以預見的是,他們的實現跟調度器和 defer 關鍵字也緊密相關。
最好的方式當然是了解編譯器究竟做了什么事情:
```
package main
func main() {
defer func() {
recover()
}()
panic(nil)
}
```
其匯編的形式為:
```
TEXT main.main(SB) /Users/changkun/dev/go-under-the-hood/demo/7-lang/panic/main.go
(...)
main.go:7 0x104e05b 0f57c0 XORPS X0, X0
main.go:7 0x104e05e 0f110424 MOVUPS X0, 0(SP)
main.go:7 0x104e062 e8d935fdff CALL runtime.gopanic(SB)
main.go:7 0x104e067 0f0b UD2
(...)
```
可以看到 panic 這個關鍵詞本質上只是一個`runtime.gopanic`調用。
而與之對應的`recover`則:
```
TEXT main.main.func1(SB) /Users/changkun/dev/go-under-the-hood/demo/7-lang/panic/main.go
(...)
main.go:5 0x104e09d 488d442428 LEAQ 0x28(SP), AX
main.go:5 0x104e0a2 48890424 MOVQ AX, 0(SP)
main.go:5 0x104e0a6 e8153bfdff CALL runtime.gorecover(SB)
(...)
```
其實也只是一個`runtime.gorecover`調用。
## 9.3.1`gopanic`和`gorecover`
正如前面所探究得來,panic 關鍵字不過是一個`gopanic`調用,接受一個參數。 在處理 panic 期間,會先判斷當前 panic 的類型,確定 panic 是否可恢復。
```
// 預先聲明的函數 panic 的實現
func gopanic(e interface{}) {
gp := getg()
// 判斷在系統棧上還是在用戶棧上
// 如果執行在系統或信號棧時,getg() 會返回當前 m 的 g0 或 gsignal
// 因此可以通過 gp.m.curg == gp 來判斷所在棧
// 系統棧上的 panic 無法恢復
if gp.m.curg != gp {
print("panic: ") // 打印
printany(e) // 打印
print("\n") // 繼續打印,下同
throw("panic on system stack")
}
// 如果正在進行 malloc 時發生 panic 也無法恢復
if gp.m.mallocing != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic during malloc")
}
// 在禁止搶占時發生 panic 也無法恢復
if gp.m.preemptoff != "" {
print("panic: ")
printany(e)
print("\n")
print("preempt off reason: ")
print(gp.m.preemptoff)
print("\n")
throw("panic during preemptoff")
}
// 在 g 鎖在 m 上時發生 panic 也無法恢復
if gp.m.locks != 0 {
print("panic: ")
printany(e)
print("\n")
throw("panic holding locks")
}
...
}
```
其他情況,panic 可以從運行時進行恢復,這時候會創建一個`_panic`實例。`_panic`類型 定義了一個`_panic`鏈表:
```
// _panic 保存了一個活躍的 panic
//
// 這個標記了 go:notinheap 因為 _panic 的值必須位于棧上
//
// argp 和 link 字段為棧指針,但在棧增長時不需要特殊處理:因為他們是指針類型且
// _panic 值只位于棧上,正常的棧指針調整會處理他們。
//
//go:notinheap
type _panic struct {
argp unsafe.Pointer // panic 期間 defer 調用參數的指針; 無法移動 - liblink 已知
arg interface{} // panic 的參數
link *_panic // link 鏈接到更早的 panic
recovered bool // 表明 panic 是否結束
aborted bool // 表明 panic 是否忽略
}
```
在創建過程中,panic 保存了對應的消息,并指向了保存在 goroutine 鏈表中先前的 panic 鏈表:
```
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
atomic.Xadd(&runningPanicDefers, 1)
```
接下來開始逐一調用當前 goroutine 的 defer 方法, 檢查用戶態代碼是否需要對 panic 進行恢復:
```
for {
// 開始逐個取當前 goroutine 的 defer 調用
d := gp._defer
// 如果沒有 defer 調用,則跳出循環
if d == nil {
break
}
// 如果 defer 是由早期的 panic 或 Goexit 開始的(并且,因為我們回到這里,這引發了新的 panic),
// 則將 defer 帶離鏈表。更早的 panic 或 Goexit 將無法繼續運行。
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)
continue
}
// 如果棧增長或者垃圾回收在 reflectcall 開始執行 d.fn 前發生
// 標記 defer 已經開始執行,但仍將其保存在列表中,從而 traceback 可以找到并更新這個 defer 的參數幀
d.started = true
// 記錄正在運行 defer 的 panic。如果在 defer 調用期間出現新的 panic,該 panic 將在列表中
// 找到 d 并標記 d._panic(該 panic)中止。
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 不會 panic. 移出 d.
if gp._defer != d {
throw("bad defer entry in panic")
}
d._panic = nil
d.fn = nil
gp._defer = d.link
pc := d.pc
sp := unsafe.Pointer(d.sp) // 必須是指針,以便在棧復制期間進行調整
freedefer(d)
if p.recovered {
atomic.Xadd(&runningPanicDefers, -1)
gp._panic = p.link
// 忽略的 panic 會被標記,但仍然保留在 g.panic 列表中
// 這里將它們移出列表
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil { // 必須由 signal 完成
gp.sig = 0
}
// 傳遞關于恢復幀的信息
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
// 調用 recover,并重新進入調度循環,不再返回
mcall(recovery)
// 如果無法重新進入調度循環,則無法恢復錯誤
throw("recovery failed")
}
}
```
這個循環說明了很多問題。首先,當 panic 發生時,如果錯誤是可恢復的錯誤,那么 會逐一遍歷該 goroutine 對應 defer 鏈表中的 defer 函數鏈表,直到 defer 遍歷完畢、 或者再次進入調度循環(recover 的 mcall 調用) 后才會停止。
defer 并非簡單的遍歷,每個在 panic 和 recover 之間的 defer 都會在這里通過`reflectcall`執行。
```
// reflectcall 使用 arg 指向的 n 個參數字節的副本調用 fn。
// fn 返回后,reflectcall 在返回之前將 n-retoffset 結果字節復制回 arg+retoffset。
// 如果重新復制結果字節,則調用者應將參數幀類型作為 argtype 傳遞,以便該調用可以在復制期間執行適當的寫障礙。
// reflect 包傳遞幀類型。在 runtime 包中,只有一個調用將結果復制回來,即 cgocallbackg1,
// 并且它不傳遞幀類型,這意味著沒有調用寫障礙。參見該調用的頁面了解相關理由。
//
// 包 reflect 通過 linkname 訪問此符號
func reflectcall(argtype *_type, fn, arg unsafe.Pointer, argsize uint32, retoffset uint32)
```
如果某個包含了 recover 的調用(即 gorecover 調用)被執行,這時`_panic`實例`p.recovered`會被標記為`true`:
```
// 執行預先聲明的函數 recover。
// 不允許分段棧,因為它需要可靠地找到其調用者的棧段。
//
// TODO(rsc): Once we commit to CopyStackAlways,
// this doesn't need to be nosplit.
//go:nosplit
func gorecover(argp uintptr) interface{} {
// 必須在 panic 期間作為 defer 調用的一部分在函數中運行。
// 必須從調用的最頂層函數( defer 語句中使用的函數)調用。
// p.argp 是最頂層 defer 函數調用的參數指針。
// 比較調用方報告的 argp,如果匹配,則調用者可以恢復。
gp := getg()
p := gp._panic
if p != nil && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}
```
同時`recover()`這個函數還會返回 panic 的保存相關信息`p.arg`。 恢復的原則取決于`gorecover`這個方法調用方報告的 argp 是否與`p.argp`相同,僅當相同才可恢復。
當`reflectcall`執行完畢后,這時如果一個 panic 是可恢復的,`p.recovered`已經被標記為`true`, 從而會通過`mcall`的方式來執行`recovery`函數來重新進入調度循環:
```
// 在發生 panic 后 defer 函數調用 recover 后展開棧。然后安排繼續運行,
// 就像 defer 函數的調用方正常返回一樣。
func recovery(gp *g) {
// 傳遞到 G 結構的 defer 信息
sp := gp.sigcode0
pc := gp.sigcode1
...
// 使 deferproc 為此 d 返回
// 這時候返回 1。調用函數將跳轉到標準的返回尾聲
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}
```
當然如果所有的 defer 都沒有指明顯式的 recover,那么這時候則直接在運行時拋出 panic 信息:
```
// 消耗完所有的 defer 調用,保守地進行 panic
// 因為在凍結之后調用任意用戶代碼是不安全的,所以我們調用 preprintpanics 來調用
// 所有必要的 Error 和 String 方法來在 startpanic 之前準備 panic 字符串。
preprintpanics(gp._panic)
fatalpanic(gp._panic) // 不應該返回
*(*int)(nil) = 0 // 無法觸及
}
```
從而完成`gopanic`的調用。
至于`preprintpanics`和`fatalpanic`無非是一些錯誤輸出,不再贅述:
```
// 在停止前調用所有的 Error 和 String 方法
func preprintpanics(p *_panic) {
defer func() {
if recover() != nil {
throw("panic while printing panic value")
}
}()
for p != nil {
switch v := p.arg.(type) {
case error:
p.arg = v.Error()
case stringer:
p.arg = v.String()
}
p = p.link
}
}
// fatalpanic 實現了不可恢復的 panic。類似于 fatalthrow,
// 要求如果 msgs != nil,則 fatalpanic 仍然能夠打印 panic 的消息并在 main 在退出時候減少 runningPanicDefers。
//
//go:nosplit
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
// 切換到系統棧來避免棧增長,如果運行時狀態較差則可能導致更糟糕的事情
systemstack(func() {
if startpanic_m() && msgs != nil {
// 有 panic 消息和 startpanic_m 則可以嘗試打印它們
// startpanic_m 設置 panic 會從阻止 main 的退出,
// 因此現在可以開始減少 runningPanicDefers 了
atomic.Xadd(&runningPanicDefers, -1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {=
// 通過在上述 systemstack 調用之外崩潰,調試器在生成回溯時不會混淆。
// 函數崩潰標記為 nosplit 以避免堆棧增長。
crash()
}
// 從系統棧退出
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // 不可達
}
```
## 小結
從 panic 和 recover 這對關鍵字的實現上可以看出,可恢復的 panic 必須要 recover 的配合。 而且,這個 recover 必須位于同一 goroutine 的直接調用鏈上,否則無法對 panic 進行恢復。
例如,如果
```
func A () {
B()
C()
}
func B() {
defer func () {
recover() // 無法恢復 panic("C")
}()
println("B")
}
func C() {
panic("C")
}
```
又例如 A 調用了 B 而 B 又調用了 C,那么 C 發生 panic 時,如果 A 要求了 recover 則仍然可以恢復。
```
func A () {
defer func () {
recover() // 可以恢復 panic("C")
}()
B()
}
func B() {
C()
}
func C() {
panic("C")
}
```
當一個 panic 被恢復后,調度并因此中斷,會重新進入調度循環,進而繼續執行 recover 后面的代碼, 包括比 recover 更早的 defer(因為已經執行過得 defer 已經被釋放, 而尚未執行的 defer 仍在 goroutine 的 defer 鏈表中),或者 recover 所在函數的調用方。
- 第一部分 :基礎篇
- 第1章 Go語言的前世今生
- 1.2 Go語言綜述
- 1.3 順序進程通訊
- 1.4 Plan9匯編語言
- 第2章 程序生命周期
- 2.1 從go命令談起
- 2.2 Go程序編譯流程
- 2.3 Go 程序啟動引導
- 2.4 主Goroutine的生與死
- 第3 章 語言核心
- 3.1 數組.切片與字符串
- 3.2 散列表
- 3.3 函數調用
- 3.4 延遲語句
- 3.5 恐慌與恢復內建函數
- 3.6 通信原語
- 3.7 接口
- 3.8 運行時類型系統
- 3.9 類型別名
- 3.10 進一步閱讀的參考文獻
- 第4章 錯誤
- 4.1 問題的演化
- 4.2 錯誤值檢查
- 4.3 錯誤格式與上下文
- 4.4 錯誤語義
- 4.5 錯誤處理的未來
- 4.6 進一步閱讀的參考文獻
- 第5章 同步模式
- 5.1 共享內存式同步模式
- 5.2 互斥鎖
- 5.3 原子操作
- 5.4 條件變量
- 5.5 同步組
- 5.6 緩存池
- 5.7 并發安全散列表
- 5.8 上下文
- 5.9 內存一致模型
- 5.10 進一步閱讀的文獻參考
- 第二部分 運行時篇
- 第6章 并發調度
- 6.1 隨機調度的基本概念
- 6.2 工作竊取式調度
- 6.3 MPG模型與并發調度單
- 6.4 調度循環
- 6.5 線程管理
- 6.6 信號處理機制
- 6.7 執行棧管理
- 6.8 協作與搶占
- 6.9 系統監控
- 6.10 網絡輪詢器
- 6.11 計時器
- 6.12 非均勻訪存下的調度模型
- 6.13 進一步閱讀的參考文獻
- 第7章 內存分配
- 7.1 設計原則
- 7.2 組件
- 7.3 初始化
- 7.4 大對象分配
- 7.5 小對象分配
- 7.6 微對象分配
- 7.7 頁分配器
- 7.8 內存統計
- 第8章 垃圾回收
- 8.1 垃圾回收的基本想法
- 8.2 寫屏幕技術
- 8.3 調步模型與強弱觸發邊界
- 8.4 掃描標記與標記輔助
- 8.5 免清掃式位圖技術
- 8.6 前進保障與終止檢測
- 8.7 安全點分析
- 8.8 分代假設與代際回收
- 8.9 請求假設與實務制導回收
- 8.10 終結器
- 8.11 過去,現在與未來
- 8.12 垃圾回收統一理論
- 8.13 進一步閱讀的參考文獻
- 第三部分 工具鏈篇
- 第9章 代碼分析
- 9.1 死鎖檢測
- 9.2 競爭檢測
- 9.3 性能追蹤
- 9.4 代碼測試
- 9.5 基準測試
- 9.6 運行時統計量
- 9.7 語言服務協議
- 第10章 依賴管理
- 10.1 依賴管理的難點
- 10.2 語義化版本管理
- 10.3 最小版本選擇算法
- 10.4 Vgo 與dep之爭
- 第12章 泛型
- 12.1 泛型設計的演進
- 12.2 基于合約的泛型
- 12.3 類型檢查技術
- 12.4 泛型的未來
- 12.5 進一步閱讀的的參考文獻
- 第13章 編譯技術
- 13.1 詞法與文法
- 13.2 中間表示
- 13.3 優化器
- 13.4 指針檢查器
- 13.5 逃逸分析
- 13.6 自舉
- 13.7 鏈接器
- 13.8 匯編器
- 13.9 調用規約
- 13.10 cgo與系統調用
- 結束語: Go去向何方?