Golang panic 和 recover 實現原理

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 調用情況,涉及的字段如下

通過查看 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
}

通過對上述代碼的執行分析,可得知 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

通過分析底層調用,可得知主要是如下幾個方法:

前面我們說了簡單的流程,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 流轉的處理代碼的。恢復流程如下:

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 所在函數的調用方。

轉自: blog.csdn.net/u010853261/article/details/102761955

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZmfwNlq5_A2RgpUSkJQXrQ