Menu

Go Panic und Recover: Laufzeit-Panics und wann du panicen solltest

Eine Panic stoppt die normale Ausführung und baut den Stack ab, wobei per defer eingeplante Aufrufe laufen. Was Panics auslöst, wie recover in einer defer-Funktion eine stoppt, welche Laufzeit-Fehlermeldungen du siehst und wann eine Panic die richtige Wahl ist.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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

MeldungUrsache
index out of range [5] with length 3Index auf Slice, Array oder String hinter dem Ende
slice bounds out of range [:7] with capacity 5Slicen über die Kapazität hinaus
invalid memory address or nil pointer dereferenceFeld lesen oder Aufruf über einen nil-Pointer
assignment to entry in nil mapSchreiben in eine Map, die nie erzeugt wurde
interface conversion: interface {} is int, not stringType Assertion mit einem Wert auf den falschen Typ
integer divide by zeroGanzzahldivision oder Modulo durch 0 (Floats ergeben stattdessen +Inf oder NaN)
close of closed channel, send on closed channelfalscher 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:

  1. a / b hat eine Panic ausgelöst.
  2. Die defer-Closure lief, und recover() gab den Panic-Wert zurück (einen runtime.Error).
  3. Der Abbau stoppte. safeDivide kehrte normal zu main zurück, mit dem benannten Ergebnis err, 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.Must und uuid.MustParse umhü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 und os.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 error zurü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 nil zurück.
  • In main die Panic einer Goroutine abfangen wollen. Jede Goroutine braucht ihr eigenes recover.
  • Alle Panics schlucken. Logge mit dem Stack (debug.Stack() aus runtime/debug) und löse bei Unerwartetem erneut eine Panic aus.
  • recover fü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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S