Skip to content

panic

panic ist eine eingebaute Funktion von Go. Wenn ein nicht behebbarer Fehler auftritt, löst das Programm oft ein panic aus, zum Beispiel bei einem häufigen Nullzeiger-Zugriff:

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

Wenn Sie den obigen Code ausführen, löst das Programm folgendes panic aus und stoppt dann:

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

In einigen Fällen rufen wir auch manuell die panic-Funktion auf, um das Programm zu beenden und schwerwiegendere Folgen zu vermeiden. Normalerweise verwenden wir auch eine andere eingebaute Funktion recover, um panic abzufangen, in Kombination mit defer:

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

Warum muss die recover-Funktion unbedingt in defer verwendet werden? Was macht recover? Diese Fragen werden im Folgenden beantwortet.

Struktur

panic hat zur Laufzeit eine entsprechende Struktur: runtime._panic. Der Aufbau ist nicht komplex:

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
}

Die Struktur ist sehr ähnlich zu der von defer:

  • link zeigt auf die nächste _panic-Struktur
  • pc und sp zeigen auf den Ausführungskontext der aufrufenden Funktion für eine spätere Wiederherstellung
  • arg ist der Parameter der panic-Funktion
  • argp zeigt auf die Parameter von defer; nach einem panic wird die Ausführung von defer ausgelöst
  • aborted gibt an, ob es zwangsweise gestoppt wurde

panic existiert wie defer als verkettete Liste in der Goroutine:

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

Panik

Egal ob wir die panic-Funktion manuell aufrufen oder das Programm ein panic auslöst, es landet immer in der Funktion runtime.gopanic:

go
func gopanic(e any)

Zu Beginn wird geprüft, ob der Parameter nil ist. Wenn ja, wird ein Fehler vom Typ runtime.PanicNilError erstellt:

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

Dann wird das aktuelle panic an den Kopf der verketteten Liste der Goroutine eingefügt:

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

Anschließend wird eine for-Schleife betreten, um die defer-Liste der aktuellen Goroutine einzeln zu verarbeiten:

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)))
    ...
}

Wenn das aktuelle defer bereits von einem anderen panic ausgelöst wurde, d.h. _defer.started == true, wird das frühere panic nicht ausgeführt. Dann wird die zum defer gehörende Funktion ausgeführt:

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

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

Nach der Ausführung wird die aktuelle defer-Struktur zurückgegeben und das nächste defer ausgeführt. Wenn alle defer-Strukturen ausgeführt wurden und keine Wiederherstellung stattgefunden hat, wird die Funktion runtime.fatalpanic aufgerufen. Diese Funktion ist unrecoverable, also nicht wiederherstellbar:

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
}

Währenddessen lässt printpanics die panic-Informationen ausgeben. Die normalerweise sichtbaren Callstack-Informationen werden von dieser Funktion ausgegeben. Schließlich wird das Programm durch die Funktion runtime.exit über den Systemaufruf _ExitProcess beendet.

Wiederherstellung

Durch den Aufruf der eingebauten Funktion recover wird dieser während der Kompilierung in einen Aufruf der Funktion runtime.gorecover umgewandelt:

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
}

Die Implementierung ist sehr einfach - sie macht nur p.recovered = true. Der Code, der tatsächlich für die Wiederherstellungslogik verantwortlich ist, befindet sich in der Funktion gopanic:

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

Die Wiederherstellungslogik wird nach der Ausführung von defer ausgeführt. Hier wird klar, warum die recover-Funktion nur in defer verwendet werden kann. Wenn recover außerhalb von defer verwendet wird, ist gp._panic gleich nil, und p.recovered wird natürlich nicht auf true gesetzt. Dann wird in der Funktion gopanic der Wiederherstellungs-Teil der Logik nicht erreicht:

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")
}

Bei der Wiederherstellung werden die bereits zwangsweise gestoppten panic in der Liste bereinigt. Dann wird die Funktion runtime.recovery aufgerufen, die durch runtime.gogo zum normalen logischen Ablauf der Benutzerfunktion zurückkehrt:

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)
}

Ein wichtiger Punkt ist diese Codezeile:

gp.sched.ret = 1

Sie setzt den ret-Wert auf 1. Aus dem Funktionskommentar von runtime.deferproc ist Folgendes zu entnehmen:

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()
}

Der vom Compiler generierte Zwischencode prüft, ob dieser Wert 1 ist. Wenn ja, wird direkt die Funktion runtime.deferreturn ausgeführt. Normalerweise wird diese Funktion erst ausgeführt, bevor die Funktion zurückkehrt. Dies erklärt auch, warum die Funktion nach recover direkt zurückkehrt.

Golang by www.golangdev.cn edit