Wie eine Panic aussieht
Eine Panic stoppt die aktuelle Funktion, führt ihre per defer eingeplanten Aufrufe aus, macht dann dasselbe im Aufrufer und so weiter den Stack hinauf. Erreicht sie die Spitze der Goroutine, stürzt das Programm ab.
Dieses Programm beendet sich mit Status 2. Die Ausgabe ist:
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
Der per defer eingeplante Aufruf lief vor dem Absturzbericht. Der Trace nennt die Goroutine, die Funktion und die Zeile, und das reicht meist, um den Bug zu finden.
Häufige Laufzeit-Panics
| Meldung | Ursache |
|---|---|
index out of range [5] with length 3 | Index auf Slice, Array oder String hinter dem Ende |
slice bounds out of range [:7] with capacity 5 | Slicen über die Kapazität hinaus |
invalid memory address or nil pointer dereference | Feld lesen oder Aufruf über einen nil-Pointer |
assignment to entry in nil map | Schreiben in eine Map, die nie erzeugt wurde |
interface conversion: interface {} is int, not string | Type Assertion mit einem Wert auf den falschen Typ |
integer divide by zero | Ganzzahldivision oder Modulo durch 0 (Floats ergeben stattdessen +Inf oder NaN) |
close of closed channel, send on closed channel | falscher Umgang mit Channels |
all goroutines are asleep - deadlock! | jede Goroutine blockiert (ein fataler Fehler, keine Panic) |
Jede davon ist ein Bug im Programm, keine Situation, die man behandelt. Die Lösung ist eine Bereichsprüfung, eine Nil-Prüfung, ein make oder eine Comma-ok-Assertion, kein recover.
Abfangen mit recover
recover() stoppt eine Panic. Es funktioniert nur, wenn es direkt in einer defer-Funktion aufgerufen wird, weil defer-Funktionen der einzige Code sind, der läuft, während eine Panic den Stack abbaut.
Ausgabe:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Was beim zweiten Aufruf passiert ist:
a / bhat eine Panic ausgelöst.- Die defer-Closure lief, und
recover()gab den Panic-Wert zurück (einenruntime.Error). - Der Abbau stoppte.
safeDividekehrte normal zumainzurück, mit dem benannten Ergebniserr, das die Closure gesetzt hat.
Das benannte Ergebnis ist es, was der defer-Funktion erlaubt, einen Fehler zurückzugeben. Ohne eins gibt die Funktion ihre Nullwerte zurück. Die Seite zu defer erklärt, wie defer-Closures Ergebnisse ändern.
recover() gibt nil zurück, wenn es keine Panic gibt, also macht die Prüfung if r != nil die defer-Funktion auf dem normalen Pfad harmlos. Außerhalb einer defer-Funktion oder in einer Funktion, die von der defer-Funktion aufgerufen wird, gibt recover nil zurück und tut nichts.
panic mit eigenem Wert
panic nimmt jeden Wert. Ein Fehler oder ein String ist typisch.
Fang ab, was du erwartest, und löse bei allem anderen erneut eine Panic aus. Jede Panic zu schlucken verbirgt echte Bugs.
Seit Go 1.21 wird panic(nil) in einen *runtime.PanicNilError umgewandelt, also bedeutet ein nil von recover() jetzt zuverlässig „keine Panic“.
Panics in Goroutinen
recover fängt nur Panics in der eigenen Goroutine ab. Eine Panic in irgendeiner Goroutine ohne recover beendet den ganzen Prozess, einschließlich main und aller anderen Goroutinen.
Die beiden Worker-Zeilen können in beliebiger Reihenfolge erscheinen; main finished kommt immer zuletzt. Ein defer recover() in main hätte das Programm nicht vor dem zweiten Worker gerettet. Deshalb fangen HTTP-Server Panics pro Request ab: net/http fängt Panics in jeder Handler-Goroutine ab, loggt sie und schließt diese Verbindung, damit ein fehlerhafter Request nicht den Server lahmlegt.
Manche Fehlschläge sind fatale Fehler, keine Panics, und lassen sich gar nicht abfangen: concurrent map writes, zu wenig Speicher und das all goroutines are asleep des Deadlock-Detektors.
Wann eine Panic die richtige Wahl ist
Die allgemeine Regel in Go: Gib Fehler zurück für alles, was zur Laufzeit schiefgehen kann, und panice nur bei Programmierfehlern. Konkret ist eine Panic angebracht, wenn:
- Eine Invariante verletzt ist. Ein
switchüber dein eigenes Enum erreicht einen Case, der nicht vorkommen kann. Weiterzumachen würde Daten beschädigen. - Ein
Must-Helfer eine fehlerhafte konstante Eingabe bekommt.regexp.MustCompile,template.Mustunduuid.MustParseumhüllen eine Funktion, die einen Fehler zurückgibt, und panicen bei einem Fehlschlag. Nutz sie für Werte, die zur Compilezeit bekannt sind, typischerweise Variablen auf Paketebene, bei denen ein Fehlschlag bedeutet, dass der Quellcode falsch ist:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Der Start nicht weitergehen kann. Eine fehlende Pflichtkonfiguration in
main. Selbst hier ist es oft sauberer, den Fehler auszugeben undos.Exit(1)aufzurufen, statt einen Stacktrace zu zeigen.
Die falsche Wahl ist eine Panic für:
- Erwartbare Fehlschläge: ungültige Nutzereingaben, eine fehlende Datei, ein Timeout. Gib einen
errorzurück; siehe Fehlerbehandlung. - Kontrollfluss: panic und recover als Exceptions über einen großen Aufrufbaum zu nutzen macht Code schwer nachvollziehbar. Die Standardbibliothek tut das intern an ein paar Stellen (im Encoder von
encoding/json), fängt aber immer vor der Rückkehr ab, sodass keine Panic das Paket verlässt. - Bibliotheks-APIs: Eine Bibliothek, die bei falschen Eingaben panict, zwingt jeden Aufrufer, recover einzubauen. Gib einen Fehler zurück.
Häufige Fehler
- recover außerhalb einer defer-Funktion aufrufen. Es gibt
nilzurück. - In
maindie Panic einer Goroutine abfangen wollen. Jede Goroutine braucht ihr eigenes recover. - Alle Panics schlucken. Logge mit dem Stack (
debug.Stack()ausruntime/debug) und löse bei Unerwartetem erneut eine Panic aus. recoverfür Schreibzugriffe auf nil-Maps oder Indizes außerhalb des Bereichs nutzen. Behebe stattdessen den Bug.
Häufig gestellte Fragen
Was ist eine Panic in Go?
Ein Fehler zur Laufzeit, der den normalen Ablauf der aktuellen Goroutine stoppt. Go führt die per defer eingeplanten Aufrufe jeder Funktion auf dem Stack aus, von innen nach außen, und wenn nichts die Panic abfängt, gibt das Programm den Panic-Wert und einen Stacktrace aus und beendet sich mit Status 2. Panics kommen von Bugs (Index außerhalb des Bereichs, Nil-Pointer-Dereferenzierung, Schreiben in eine nil-Map) oder von einem expliziten Aufruf panic(v).
Wie fängt man in Go eine Panic ab?
Ruf recover() in einer defer-Funktion auf: defer func() { if r := recover(); r != nil { ... } }(). Es gibt den an panic übergebenen Wert zurück und stoppt den Abbau des Stacks, sodass die Funktion, die es eingeplant hat, normal zu ihrem Aufrufer zurückkehrt. An jeder anderen Stelle gibt recover nil zurück und tut nichts.
Kann ich eine Panic aus einer anderen Goroutine abfangen?
Nein. recover stoppt nur eine Panic in der Goroutine, in der es läuft. Eine Panic in einer Goroutine, die du gestartet hast und in der kein recover steht, bringt das ganze Programm zum Absturz. Jede Goroutine, die panicen könnte, braucht ihr eigenes recover per defer.
Wann sollte ich panic verwenden statt einen Fehler zurückzugeben?
Für Bugs und unmögliche Zustände, nicht für erwartbare Fehlschläge. Falsche Eingaben, fehlende Dateien und Netzwerkfehler sind Fehler. Eine Panic ist sinnvoll, wenn eine Invariante verletzt ist, wenn ein Must-Helfer eine Konstante bekommt, die immer gültig sein sollte (regexp.MustCompile), oder wenn das Programm gar nicht erst starten kann. Bibliotheken sollten keine Panics aus ihrer öffentlichen API entweichen lassen.