Skip to content

panic

panic是 go 的內置函數,當遇到不可恢復的錯誤時,程序往往就會拋出panic,比如常見的空指針訪問

go
func main() {
  var a *int
  *a = 1
}

運行上面這段代碼,程序就會拋出如下的panic,然後程序就會停止。

panic: runtime error: invalid memory address or nil pointer dereference

在一些情況下,我們也會手動調用panic函數來讓程序退出,從而避免更嚴重的後果。平時也會用另一個內置函數recover來捕獲panic,並配合defer使用。

go
func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  var a *int
  *a = 1
}

為什麼recover函數一定要在defer裡面使用,recover做了些什麼工作,這些問題都會在下面的內容得到解答。

結構

panic在運行時也有對應的結構進行表示,那就是runtime._panic,其結構並不復雜,如下。

go
type _panic struct {
  argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
  arg       any            // argument to panic
  link      *_panic        // link to earlier panic
  pc        uintptr        // where to return to in runtime if this panic is bypassed
  sp        unsafe.Pointer // where to return to in runtime if this panic is bypassed
  recovered bool           // whether this panic is over
  aborted   bool           // the panic was aborted
  goexit    bool
}

它的結構與defer非常類似,

  • link指向下一個_panic結構,
  • pcsp指向調用函數的執行現場便於日後恢復,
  • arg就是panic函數的參數,
  • argp指向defer的參數,panic發生後便會觸發defer的執行
  • aborted表示其是否被強制停止

panicdefer一樣,也是以鏈表的形式存在於協程中

go
type g struct {
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
}

恐慌

無論是我們主動調用panic函數,抑或是程序發生的panic,最終都會走入runtime.gopanic函數中

go
func gopanic(e any)

在開始時,首先會檢測參數是否為nil,如果是nil的話就會 new 一個runtime.PanicNilError類型的錯誤

go
if e == nil {
    if debug.panicnil.Load() != 1 {
        e = new(PanicNilError)
    } else {
        panicnil.IncNonDefault()
    }
}

然後將當前的panic加入協程的鏈表頭部

go
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

隨後進入for循環開始逐個處理當前協程的defer鏈表

go
for {
    d := gp._defer
    if d == nil {
      break
    }

    if d.started {
      if d._panic != nil {
        d._panic.aborted = true
      }
      d._panic = nil
    }
    d.started = true
        d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
    ...
}

如果當前的defer已經被其它的panic觸發了,即_defer.started == true,那麼較早的panic將不會執行。然後再執行defer對應的函數

go
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil

d.fn = nil
gp._defer = d.link
freedefer(d)

執行完後回收當前的defer結構,繼續執行下一個defer,當執行完全部的defer結構後且期間沒有被恢復,就會進入runtime.fatalpanic函數,該函數是unrecoverable即不可恢復的

go
func fatalpanic(msgs *_panic) {
  pc := getcallerpc()
  sp := getcallersp()
  gp := getg()
  var docrash bool
  systemstack(func() {
    if startpanic_m() && msgs != nil {
      runningPanicDefers.Add(-1)
      printpanics(msgs)
    }

    docrash = dopanic_m(gp, pc, sp)
  })

  if docrash {
    crash()
  }

  systemstack(func() {
    exit(2)
  })

  *(*int)(nil) = 0 // not reached
}

在這期間會讓printpanics打印panic的信息,我們通常看到的調用棧信息就是由它輸出的,最後由runtime.exit函數通過系統調用_ExitProcess退出程序。

恢復

通過調用內置函數recover,編譯期間會變為對runtime.gorecover函數的調用

go
func gorecover(argp uintptr) any {
  gp := getg()
  p := gp._panic
  if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
    p.recovered = true
    return p.arg
  }
  return nil
}

它的實現非常簡單,只干了p.recovered = true這麼一件事,而真正負責處理恢復邏輯的代碼實際上在gopanic函數裡

go
for {
    ...
      d.fn()
      ...
    if p.recovered {
      ...
    }
}

恢復的邏輯在defer執行後,到這裡也就明白了為什麼recover函數只能在defer中使用,如果在defer之外使用recover函數的話gp._panic就等於nil,自然p.recovered就不會被設置為true,那麼在gopanic函數中也就不會走到恢復這部分邏輯裡面來。

go
if p.recovered {
    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")
}

恢復時會清理鏈表中那些已經被強制停止的panic,然後進入runtime.recovery函數中,由runtime.gogo回到用戶函數的正常邏輯流程中

go
func recovery(gp *g) {
  // Info about defer passed in G struct.
  sp := gp.sigcode0
  pc := gp.sigcode1

  gp.sched.sp = sp
  gp.sched.pc = pc
  gp.sched.lr = 0
  gp.sched.ret = 1
  gogo(&gp.sched)
}

然後有一個重點需要注意的是這行代碼

gp.sched.ret = 1

它將ret值設置成為了 1,從runtime.deferproc的函數注釋中可以看代碼下面這些內容

go
func deferproc(fn func()) {
    ...
  // deferproc returns 0 normally.
  // a deferred func that stops a panic
  // makes the deferproc return 1.
  // the code the compiler generates always
  // checks the return value and jumps to the
  // end of the function if deferproc returns != 0.
  return0()
}

編譯器生成的中間代碼會檢查該值是否為 1,如果是的話就會直接執行runtime.deferreturn函數,通常該函數只有在函數返回之前才會執行,這也說明了為什麼recover過後函數會直接返回。

Golang學習網由www.golangdev.cn整理維護