Go Panic and Recover In Depth: Alles, was du wissen musst!
Lukas Schneider
DevOps Engineer · Leapcell

Detaillierte Erläuterung der Schlüsselwörter panic und recover in der Go-Sprache
In der Go-Sprache gibt es zwei Schlüsselwörter, die oft paarweise auftreten – panic und recover. Diese beiden Schlüsselwörter stehen in enger Beziehung zu defer. Sie sind beide integrierte Funktionen in der Go-Sprache und bieten komplementäre Funktionen.
I. Grundlegende Funktionen von panic und recover
- panic: Sie kann den Kontrollfluss des Programms ändern. Nach dem Aufruf von panic wird die restliche Ausführung des aktuellen Funktion sofort gestoppt, und das Defer des Aufrufers wird rekursiv in der aktuellen Goroutine ausgeführt.
- recover: Sie kann den durch Panic verursachten Programmabsturz stoppen. Es ist eine Funktion, die nur in Defer wirksam werden kann. Der Aufruf in anderen Bereichen hat keine Auswirkung.
II. Phänomene bei der Verwendung von panic und recover
(I) panic löst nur das Defer der aktuellen Goroutine aus
Der folgende Code demonstriert dieses Phänomen:
func main() { defer println("in main") go func() { defer println("in goroutine") panic("") }() time.Sleep(1 * time.Second) }
Das Ergebnis der Ausführung ist wie folgt:
$ go run main.go
in goroutine
panic:
...
Bei der Ausführung dieses Codes wird festgestellt, dass die Defer-Anweisung in der Hauptfunktion nicht ausgeführt wird und nur das Defer in der aktuellen Goroutine ausgeführt wird. Da die runtime.deferproc, die dem Defer-Schlüsselwort entspricht, die verzögerte Aufruffunktion mit der Goroutine verbindet, in der sich der Aufrufer befindet, wird beim Absturz des Programms nur die verzögerte Aufruffunktion der aktuellen Goroutine aufgerufen.
(II) recover wird nur wirksam, wenn es in defer aufgerufen wird
Der folgende Code spiegelt diese Funktion wider:
func main() { defer fmt.Println("in main") if err := recover(); err != nil { fmt.Println(err) } panic("unknown err") }
Das Ergebnis der Ausführung ist:
$ go run main.go
in main
panic: unknown err
goroutine 1 [running]:
main.main()
...
exit status 2
Durch sorgfältige Analyse dieses Prozesses kann festgestellt werden, dass recover nur dann wirksam wird, wenn es nach einem Panic-Ereignis aufgerufen wird. Im obigen Kontrollfluss wird recover jedoch vor panic aufgerufen, was die Bedingungen für das Wirksamwerden nicht erfüllt. Daher muss das recover-Schlüsselwort in defer verwendet werden.
(III) panic ermöglicht mehrere verschachtelte Aufrufe in defer
Der folgende Code zeigt, wie panic mehrmals in einer Defer-Funktion aufgerufen werden kann:
func main() { defer fmt.Println("in main") defer func() { defer func() { panic("panic again and again") }() panic("panic again") }() panic("panic once") }
Das Ergebnis der Ausführung ist wie folgt:
$ go run main.go
in main
panic: panic once
panic: panic again
panic: panic again and again
goroutine 1 [running]:
...
exit status 2
Aus dem Ausgaberesultat des obigen Programms kann festgestellt werden, dass mehrere Aufrufe von panic im Programm die normale Ausführung der defer-Funktion nicht beeinträchtigen. Daher ist es im Allgemeinen sicher, defer für die Finalisierungsarbeiten zu verwenden.
III. Datenstruktur von panic
Das Schlüsselwort panic im Quellcode der Go-Sprache wird durch die Datenstruktur runtime._panic dargestellt. Jedes Mal, wenn panic aufgerufen wird, wird eine Datenstruktur wie die folgende erstellt, um relevante Informationen zu speichern:
type _panic struct { argp unsafe.Pointer arg interface{} link *_panic recovered bool aborted bool pc uintptr sp unsafe.Pointer goexit bool }
- argp: Es ist ein Zeiger auf den Parameter, wenn defer aufgerufen wird.
- arg: Es ist der Parameter, der beim Aufruf von panic übergeben wird.
- link: Es zeigt auf die früher aufgerufene runtime._panic Struktur.
- recovered: Es gibt an, ob die aktuelle runtime._panic durch recover wiederhergestellt wurde.
- aborted: Es gibt an, ob die aktuelle Panic zwangsweise beendet wurde.
Aus dem Link-Feld in der Datenstruktur kann abgeleitet werden, dass die Panic-Funktion kontinuierlich mehrmals aufgerufen werden kann und diese über den Link eine verkettete Liste bilden können.
Die drei Felder pc, sp und goexit in der Struktur werden alle eingeführt, um die Probleme zu beheben, die durch runtime.Goexit verursacht werden. runtime.Goexit kann nur die Goroutine beenden, die diese Funktion aufruft, ohne andere Goroutinen zu beeinträchtigen. Diese Funktion wird jedoch durch panic und recover in defer aufgehoben. Die Einführung dieser drei Felder soll sicherstellen, dass diese Funktion auf jeden Fall wirksam wird.
IV. Prinzip des Programmabsturzes
Der Compiler wandelt das Schlüsselwort panic in runtime.gopanic um. Der Ausführungsprozess dieser Funktion umfasst die folgenden Schritte:
- Erstellen Sie eine neue runtime._panic und fügen Sie diese am Anfang der _panic verketten Liste der Goroutine hinzu, in der sie sich befindet.
- Rufen Sie kontinuierlich runtime._defer aus der _defer verketten Liste der aktuellen Goroutine in einer Schleife ab und rufen Sie runtime.reflectcall auf, um die verzögerte Aufruffunktion auszuführen.
- Rufen Sie runtime.fatalpanic auf, um das gesamte Programm abzubrechen.
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 } d._panic = (*_panic)(noescape(unsafe.Pointer(&p))) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) if p.recovered { ... } } fatalpanic(gp._panic) *(*int)(nil) = 0 }
Es ist zu beachten, dass drei relativ wichtige Codeteile in der obigen Funktion ausgelassen wurden:
- Der Code im wiederherstellungs Zweig zum Wiederherstellen des Programms.
- Der Code zur Optimierung der Leistung des Defer-Aufrufs durch Inlining.
- Der Code zum Beheben der abnormalen Situation von runtime.Goexit.
In Version 1.14 hat die Go-Sprache den Konflikt zwischen rekursivem Panic und recover und runtime.Goexit durch die Übermittlung von runtime gelöst: Stellen Sie sicher, dass Goexit nicht durch einen rekursiven Panic/recover abgebrochen werden kann.
runtime.fatalpanic implementiert einen Programmabsturz, der nicht wiederhergestellt werden kann. Vor dem Abbruch des Programms werden alle Panic-Nachrichten und die während des Aufrufs übergebenen Parameter über runtime.printpanics ausgegeben:
func fatalpanic(msgs *_panic) { pc := getcallerpc() sp := getcallersp() gp := getg() if startpanic_m() && msgs != nil { atomic.Xadd(&runningPanicDefers, -1) printpanics(msgs) } if dopanic_m(gp, pc, sp) { crash() } exit(2) }
Nach dem Ausgeben der Absturznachricht wird runtime.exit aufgerufen, um das aktuelle Programm zu beenden und den Fehlercode 2 zurückzugeben. Der normale Beendigung des Programms wird auch über runtime.exit implementiert.
V. Prinzip der Absturzwiederherstellung
Der Compiler wandelt das Schlüsselwort recover in runtime.gorecover um:
func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil &&!p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }
Die Implementierung dieser Funktion ist sehr einfach. Wenn die aktuelle Goroutine panic nicht aufgerufen hat, gibt diese Funktion direkt nil zurück, was auch der Grund dafür ist, dass die Absturzwiederherstellung fehlschlägt, wenn sie in einem Nicht-Defer aufgerufen wird. Unter normalen Umständen wird das recovered-Feld von runtime._panic geändert, und die Wiederherstellung des Programms wird von der runtime.gopanic-Funktion verarbeitet:
func gopanic(e interface{}) { ... for { // Execute the deferred call function, which may set p.recovered = true ... pc := d.pc sp := unsafe.Pointer(d.sp) ... 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") } { ... }
Der obige Code lässt die Inlining-Optimierung von Defer aus. Er entnimmt den Programmzähler pc und den Stapelzeiger sp von runtime._defer und ruft die runtime.recovery-Funktion auf, um die Planung der Goroutine auszulösen. Vor der Planung werden der sp, pc und der Rückgabewert der Funktion vorbereitet:
func recovery(gp *g) { 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) }
Wenn das Schlüsselwort defer aufgerufen wird, wurden der Stapelzeiger sp und der Programmzähler pc zum Zeitpunkt des Aufrufs bereits in der runtime._defer Struktur gespeichert. Die runtime.gogo-Funktion springt hier zurück zu der Position, an der das Schlüsselwort defer aufgerufen wurde.
runtime.recovery setzt den Rückgabewert der Funktion während des Planungsprozesses auf 1. Aus den Kommentaren von runtime.deferproc kann festgestellt werden, dass der vom Compiler generierte Code, wenn der Rückgabewert der runtime.deferproc Funktion 1 ist, direkt vor die Rückgabe der Aufruferfunktion springt und runtime.deferreturn ausführt:
func deferproc(siz int32, fn *funcval) { ... return0() }
Nach dem Springen zur runtime.deferreturn Funktion wurde das Programm von der Panic wiederhergestellt und führt die normale Logik aus, und die runtime.gorecover Funktion kann auch den arg Parameter entnehmen, der beim Aufruf von panic aus der runtime._panic Struktur übergeben wurde, und ihn an den Aufrufer zurückgeben.
VI. Zusammenfassung
Die Analyse des Programmabsturzes und des Wiederherstellungsprozesses ist eher knifflig, und der Code ist nicht besonders leicht zu verstehen. Hier ist eine einfache Zusammenfassung des Programmabsturzes und des Wiederherstellungsprozesses:
- Der Compiler ist für die Arbeit der Konvertierung von Schlüsselwörtern verantwortlich. Er konvertiert panic und recover in runtime.gopanic bzw. runtime.gorecover, konvertiert defer in die runtime.deferproc Funktion und ruft am Ende der Funktion, die defer aufruft, die runtime.deferreturn Funktion auf.
- Wenn während des laufenden Prozesses die runtime.gopanic Methode gefunden wird, wird die runtime._defer Struktur nacheinander aus der verketten Liste der Goroutine entnommen und ausgeführt.
- Wenn beim Aufruf der verzögerten Ausführungsfunktion runtime.gorecover gefunden wird, wird _panic.recovered als true markiert und der Parameter des Panic zurückgegeben.
- Nach Beendigung dieses Aufrufs entnimmt runtime.gopanic den Programmzähler pc und den Stapelzeiger sp aus der runtime._defer Struktur und ruft die runtime.recovery Funktion auf, um das Programm wiederherzustellen.
- runtime.recovery springt gemäß dem übergebenen pc und sp zu runtime.deferproc zurück.
- Der automatisch vom Compiler generierte Code findet heraus, dass der Rückgabewert von runtime.deferproc nicht 0 ist. Zu diesem Zeitpunkt springt er zu runtime.deferreturn zurück und stellt den normalen Ausführungsfluss wieder her.
- Wenn runtime.gorecover nicht gefunden wird, werden nacheinander alle runtime._defer durchlaufen und schließlich runtime.fatalpanic aufgerufen, um das Programm abzubrechen, die Parameter des Panic auszugeben und den Fehlercode 2 zurückzugeben.
Der Analyseprozess umfasst viel Wissen auf der untersten Ebene der Sprache, und der Quellcode ist auch relativ schwer zu lesen. Er ist voll von unkonventionellen Kontrollflüssen, die über den Programmzähler hin und her springen. Es ist jedoch immer noch sehr hilfreich, um den Ausführungsfluss des Programms zu verstehen.
Leapcell: Die Serverlose Plattform der nächsten Generation für Golang-Hosting, asynchrone Aufgaben und Redis
Abschließend möchte ich die am besten geeignete Bereitstellungsplattform empfehlen: Leapcell
1. Unterstützung mehrerer Sprachen
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ