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:
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 dereferenceIn 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:
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:
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:
linkzeigt auf die nächste_panic-Strukturpcundspzeigen auf den Ausführungskontext der aufrufenden Funktion für eine spätere Wiederherstellungargist der Parameter derpanic-Funktionargpzeigt auf die Parameter vondefer; nach einempanicwird die Ausführung vondeferausgelöstabortedgibt an, ob es zwangsweise gestoppt wurde
panic existiert wie defer als verkettete Liste in der Goroutine:
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:
func gopanic(e any)Zu Beginn wird geprüft, ob der Parameter nil ist. Wenn ja, wird ein Fehler vom Typ runtime.PanicNilError erstellt:
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:
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:
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:
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:
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:
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:
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:
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:
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 = 1Sie setzt den ret-Wert auf 1. Aus dem Funktionskommentar von runtime.deferproc ist Folgendes zu entnehmen:
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.
