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 は後日の回復のために呼び出し関数の実行現場を指します
  • argpanic 関数のパラメータです
  • argpdefer のパラメータを指し、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 の場合、runtime.PanicNilError 型のエラーを new します。

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
}

この間に printpanicspanic の情報を出力します。通常見る呼び出しスタック情報はこれによって出力されます。最後に 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 という 1 つのことしか行っていません。実際には回復ロジックを処理するコードは gopanic 関数内にあります。

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

回復ロジックは defer 実行後にあり、ここでなぜ recover 関数が defer 内でのみ使用できるのかがわかりました。defer 外で recover 関数を使用すると、gp._panicnil になり、当然 p.recoveredtrue に設定されず、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 かどうかをチェックし、1 の場合は直接 runtime.deferreturn 関数を実行します。通常、この関数は関数が返却する前にのみ実行されます。这也说明了为什么 recover 过后函数会直接返回。

Golang学习网由www.golangdev.cn整理维护