panic und recover sind in Go der letzte Ausweg, nicht das normale Fehler-Werkzeug. Wer aus anderen Sprachen try/catch mitbringt, greift instinktiv danach — und schreibt sofort un-idiomatischen Go-Code. Die Sprache trennt scharf zwischen erwarteten Fehlern (Rückgabewert error, der Aufrufer entscheidet) und unwiederbringlichen Zuständen (panic, Programm wird abgebaut). Dieser Artikel zeigt, was panic mechanisch tut, wie recover es im Ausnahmefall fängt, und vor allem: an welcher Stelle die beiden in einer ehrlichen Go-Codebasis vorkommen — und an welcher nicht.
Was panic mechanisch tut
panic(v any) ist ein Built-in. Der Aufruf bricht den normalen Programmfluss in der aktuellen Funktion sofort ab. Bevor die Funktion wirklich verschwindet, laufen jedoch ihre deferred-Funktionen in LIFO-Reihenfolge — wie bei einem normalen return. Anschließend wird der Stack abwärts gelaufen: für jede aufgerufene Funktion gilt dieselbe Regel — defer-Stack abarbeiten, dann an den nächsten Frame oben weiterreichen. Am Ende, wenn die Goroutine keinen Frame mehr hat, terminiert das Programm mit einem Stack-Trace und einem Exit-Code ungleich null.
Die pkg.go.dev/builtin-Beschreibung von panic ist die präziseste Quelle:
When a function F calls panic, normal execution of F stops immediately. Any functions whose execution was deferred by F are run in the usual way, and then F returns to its caller. To the caller G, the invocation of F then behaves like a call to panic, terminating G's execution and running any deferred functions. This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code.
Das Programm „dies" — wie Effective Go es nennt — wenn das Unwinding das obere Ende der Goroutine erreicht, ohne dass recover eingegriffen hat.
package main
import "fmt"
func inner() {
defer fmt.Println(" inner defer")
panic("etwas ist kaputt")
}
func outer() {
defer fmt.Println("outer defer")
inner()
fmt.Println("dieser Print wird NIE ausgeführt")
}
func main() {
defer fmt.Println("main defer")
outer()
} inner defer
outer defer
main defer
panic: etwas ist kaputtWichtig in der Ausgabe: die defer-Statements laufen alle — auch in outer und main. Was nicht läuft, sind die regulären Statements nach dem panic-Auslöser. Das Unwinding ist also kein „abrupt stop", sondern ein geordneter Abbau, der den Aufräum-Code respektiert.
Wer panic auslöst
Zwei Quellen lösen panics aus:
Explizit aus dem User-Code. Der direkte Aufruf panic("...") mit beliebigem Argument (any-Typ). Üblich sind Strings oder bereits konstruierte error-Werte.
Implizit aus der Runtime. Eine Reihe von Operationen panicen, wenn die Voraussetzungen nicht stimmen. Die häufigsten Runtime-Panics:
| Auslöser | Typischer Fehler-String |
|---|---|
| Nil-Dereferenzierung | runtime error: invalid memory address or nil pointer dereference |
| Index out of range | runtime error: index out of range [5] with length 3 |
| Slice out of bounds | runtime error: slice bounds out of range |
| Division durch null (Integer) | runtime error: integer divide by zero |
| Ungültige Type Assertion | interface conversion: T is not U |
| Concurrent Map-Write | fatal error: concurrent map writes (nicht recoverbar) |
| Channel auf nil-Channel schließen | close of nil channel |
| Senden auf geschlossenen Channel | send on closed channel |
Beachte die letzte Zeile in der Tabelle — der Eintrag mit fatal error. concurrent map writes ist kein gewöhnlicher panic: die Runtime stuft ihn als „fatal" ein, und recover kann ihn nicht auffangen. Genauso verhält es sich mit Stack-Overflows und manchen Memory-Allocation-Fehlern. Es gibt also panics, die selbst der disziplinierteste Recover-Pattern nicht rettet.
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recovered: %v (Typ %T)\n", r, r)
}
}()
var xs []int
_ = xs[10] // implizit ausgelöster Runtime-Panic
}recovered: runtime error: index out of range [10] with length 0 (Typ *runtime.boundsError)Die Runtime gibt einen Wert vom Typ runtime.Error (interface), konkret z. B. *runtime.boundsError, in den panic. Dein recover-Code kann mit errors.As oder einer Type-Assertion auf runtime.Error prüfen, ob er es mit einem Runtime-Panic oder mit einem expliziten Programm-Panic zu tun hat.
Spec-Wortlaut
Die Built-in-Doku zu panic formuliert das Verhalten knapp und verbindlich:
The panic built-in function stops normal execution of the current goroutine. […] This continues until all functions in the executing goroutine have stopped, in reverse order. At that point, the program is terminated with a non-zero exit code. This termination sequence is called panicking and can be controlled by the built-in function recover.
Zwei Details, die in der Praxis Wirkung haben:
- „current goroutine" — der Stack-Unwind betrifft nur die Goroutine, in der
panicaufgerufen wurde. Andere Goroutinen laufen weiter, bis der Programm-Tod das gesamte Prozess-Image abräumt. - „non-zero exit code" — Go beendet sich nach einem ungefangenen panic mit Status 2. Wer ein Wrapper-Script schreibt, sieht das.
Seit Go 1.21 ist außerdem panic(nil) selbst ein Laufzeitfehler — die Runtime ersetzt es durch einen typisierten *runtime.PanicNilError. Vorher konnte recover bei panic(nil) nicht zwischen „nichts passiert" und „mit nil paniciert" unterscheiden — das hat zu echten Bugs geführt.
Was recover macht
recover() ist das Pendant zu panic. Es ist ein Built-in mit Signatur func recover() any. Die builtin-Doku ist auch hier präzise:
Executing a call to recover inside a deferred function (but not any function called by it) stops the panicking sequence by restoring normal execution and retrieves the error value passed to the call of panic. If recover is called outside the deferred function it will not stop a panicking sequence. In this case, or when the goroutine is not panicking, recover returns nil.
Drei Bedingungen müssen erfüllt sein, damit recover wirkt:
- Es muss innerhalb einer deferred-Funktion aufgerufen werden. Direkt — nicht aus einer Funktion, die von der deferred-Funktion aufgerufen wird.
- Die Goroutine muss aktuell paniciert sein. Sonst gibt
recovereinfachnilzurück. - Es muss dieselbe Goroutine sein.
recoverin Goroutine A fängt keinen Panic aus Goroutine B.
Wenn alle drei Bedingungen erfüllt sind, passiert dreierlei: das Unwinding stoppt, recover gibt das ursprüngliche panic-Argument zurück, und der normale Programmfluss geht im Aufrufer der deferred-Funktion weiter.
package main
import "fmt"
func mayPanic() {
panic("ups")
}
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("aufgefangen:", r)
}
}()
mayPanic()
fmt.Println("unerreichbar")
}
func main() {
safeCall()
fmt.Println("zurück in main — Programm läuft weiter")
}aufgefangen: ups
zurück in main — Programm läuft weiterBeachte den Ablauf: nach panic("ups") läuft die deferred-Funktion, recover schluckt den Panic, safeCall returnt normal zurück nach main, und main macht weiter. Die Zeile fmt.Println("unerreichbar") läuft tatsächlich nie — recover ist kein „goto" zurück hinter den Panic-Punkt, sondern ein Auffangen am Funktions-Ausgang.
Das kanonische Pattern
Die übliche Form ist eine anonyme deferred-Funktion direkt am Anfang des Bereichs, in dem du auffangen willst:
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
// panic-Wert in error konvertieren
err = fmt.Errorf("panic in safeOperation: %v", r)
}
}()
// ... Code, der panicen darf ...
return nil
}Zwei Details verdienen Aufmerksamkeit:
- Named Return Value
(err error). Der Rückgabewert ist benannt, damit die deferred-Funktion ihn nach dem Panic noch setzen kann. Eine unbenannte Signaturfunc safeOperation() errorwürde das Setzen aus dem defer nicht zurückgeben — der Caller bekäme den Zero-Value. - Inline-Funktion, nicht referenzierte Methode. Der
defer func() { ... }()läuft als Closure, hat Zugriff auf den benannten Rückgabewert.defer recover()direkt funktioniert nicht —recovermuss aus einer eigenen deferred-Funktion heraus gerufen werden, nicht selbst die deferred-Funktion sein.
Wer Type-Assertion auf den Recovery-Wert macht, kann zwischen Programmierer-Panic und Runtime-Panic unterscheiden:
defer func() {
switch r := recover(); v := r.(type) {
case nil:
// kein Panic — alles gut
case runtime.Error:
// Index, Nil-Deref, Type-Assertion etc. — wahrscheinlich Bug
log.Printf("runtime panic: %v", v)
err = fmt.Errorf("interner Fehler: %w", v)
case error:
// explizit als error paniciert
err = v
case string:
// explizit als String paniciert
err = errors.New(v)
default:
// sonst — generisches Wrap
err = fmt.Errorf("panic: %v", v)
}
}()Wann panic richtig ist
Es gibt einen schmalen Satz an Situationen, in denen panic die idiomatisch richtige Wahl ist:
Unwiederbringlicher Init-Fehler. Beim Programmstart ist ein fehlgeschlagener init(), eine kaputte Pflicht-Config oder eine fehlende kritische Resource ein guter Panic-Kandidat — das Programm kann ohnehin nicht sinnvoll weiterlaufen. Beispiel: regexp.MustCompile, template.Must. Die Must*-Konvention markiert genau das.
Invariant-Verletzung. Eine Code-Stelle, die per Design „kann nicht passieren" ist. Wenn der default-Branch eines vollständigen switch über einen Enum-Wert getroffen wird, ist das ein Bug — panic ist die ehrliche Antwort. Ein stilles Weiterlaufen würde nur Folge-Fehler verursachen.
Library-Programmierer-Fehler. Wenn jemand deine Library falsch aufruft — etwa einen nil-Pointer übergibt, wo die Doku explizit „must not be nil" sagt — ist panic legitim. Das ist kein Laufzeit-Fehler, das ist ein Compile-Zeit-Bug, der nur zur Laufzeit sichtbar wurde.
package config
import (
"regexp"
"text/template"
)
// MustCompile-Idiom: panic bei kaputter Config, weil der Caller den Wert
// ohnehin als Konstante zur Programmstart-Zeit setzt. Ein error-Return
// wäre hier nur Boilerplate ohne Wert.
var emailRegex = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
var welcomeTmpl = template.Must(template.New("welcome").Parse(`
Hallo {{.Name}}!
`))type State int
const (
StateIdle State = iota
StateRunning
StateDone
)
func (s State) String() string {
switch s {
case StateIdle:
return "idle"
case StateRunning:
return "running"
case StateDone:
return "done"
default:
// Kann nur passieren, wenn jemand einen neuen State-Wert
// hinzugefügt und vergessen hat, hier zu ergänzen.
panic(fmt.Sprintf("unbekannter State: %d", s))
}
}Wann panic FALSCH ist
Spiegelbildlich — und in der Praxis viel wichtiger — sind die Situationen, in denen panic die falsche Antwort ist:
- Fehlende Datei.
os.Opengibt einenerrorzurück. Wer hier paniciert, signalisiert dem Aufrufer „das Programm muss sterben" — obwohl der Aufrufer vielleicht einfach auf eine andere Datei ausweichen will. - Ungültige User-Eingabe. Eine kaputte JSON-Payload aus einem HTTP-Request ist normaler Programmfluss. Antwort:
400 Bad Request, nicht panic. - Netzwerk-Fehler. Timeout, DNS-Fehler, Connection-Reset — das sind erwartete Zustände in jedem Service. Retry-Logik, Circuit-Breaker, Fallback — das alles braucht erreichbare error-Werte, keinen Stack-Unwind.
- Validierung. „Alter muss positiv sein" ist kein Bug, das ist Business-Logik.
errors.New("alter muss positiv sein")ist die Antwort. - Bibliothek-API. Die
go.dev/blog/defer-panic-and-recover-Konvention sagt es klar: „The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values."
Faustregel: Wenn der Aufrufer den Fehler sinnvoll behandeln könnte, gib einen error zurück. Panic ist für die Fälle reserviert, in denen es nichts mehr zu behandeln gibt.
// ANTI-PATTERN — panic für normalen Fehler-Pfad
func loadConfig(path string) Config {
data, err := os.ReadFile(path)
if err != nil {
panic(err) // FALSCH: Caller kann das nicht behandeln
}
// ...
}
// RICHTIG — error zurückgeben, Caller entscheidet
func loadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("config laden: %w", err)
}
// ...
}Stack-Trace-Verhalten
Wenn ein Panic durchschlägt, schreibt die Runtime einen Stack-Trace auf stderr. Was darin enthalten ist, hängt von der GOTRACEBACK-Umgebungsvariable ab:
GOTRACEBACK | Inhalt |
|---|---|
none | Kein Stack-Trace, nur die panic-Message |
single (Default) | Stack der pancenden Goroutine, gefiltert |
all | Stacks aller User-Goroutinen |
system | Stacks aller Goroutinen inkl. Runtime-Goroutinen |
crash | Wie system + erzeugt einen Core-Dump |
In Production setzt man oft GOTRACEBACK=all als Default, damit man bei Crashes auch die anderen Goroutinen sieht — gerade bei Deadlocks ist die paniccende Goroutine selten die einzig interessante.
Programmatisch kommst du an den Stack-Trace mit runtime/debug.Stack(). Das ist der übliche Weg, einen Trace innerhalb eines recover zu loggen, ohne das Programm sterben zu lassen:
package main
import (
"fmt"
"runtime/debug"
)
func deep() {
panic("etwas tief drinnen ist kaputt")
}
func middle() {
deep()
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("recover: %v\n", r)
fmt.Println("--- Stack ---")
fmt.Println(string(debug.Stack()))
}
}()
middle()
}Der debug.Stack()-Trace zeigt auch den runtime/debug.Stack-Frame selbst — das ist normal, weil debug.Stack einfach den aktuellen Goroutine-Stack zur Aufrufzeit serialisiert. Für Logging in Production reicht das vollkommen aus.
recover außerhalb von defer ist no-op
Eine Stolperfalle, die jeden Go-Anfänger einmal erwischt: recover direkt im normalen Code-Fluss aufzurufen. Die Built-in-Doku sagt klar: „If recover is called outside the deferred function it will not stop a panicking sequence."
package main
import "fmt"
// FALSCH — recover wird direkt aufgerufen, nicht aus defer
func wrong() {
r := recover() // immer nil
if r != nil {
fmt.Println("nie erreicht:", r)
}
panic("kaboom")
}
// FALSCH — recover in einer Helfer-Funktion, die aus defer gerufen wird
func recoverHelper() {
if r := recover(); r != nil {
fmt.Println("auch nie erreicht:", r)
}
}
func wrong2() {
defer recoverHelper() // recover ist hier indirekt -> no-op
panic("kaboom2")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("main fängt:", r)
}
}()
wrong2()
}main fängt: kaboom2Beachte den zweiten Fall: recoverHelper wird zwar aus einem defer aufgerufen, aber recover selbst läuft innerhalb von recoverHelper, nicht direkt im defer. Die Spec ist hier explizit — recover muss „inside a deferred function (but not any function called by it)" aufgerufen werden. Konsequenz: das Pattern bleibt die anonyme Closure direkt im defer, ohne Indirektion.
recover fängt nur in DERSELBEN Goroutine
Eine weitere häufige Falschannahme: ein zentraler recover im Hauptprogramm fängt auch Panics aus Goroutinen ab, die go ... startet. Das ist falsch — jede Goroutine hat ihren eigenen Stack, und Panics einer Goroutine durchlaufen nur deren Stack. Erreicht der Panic das obere Ende dieses Stacks, stirbt das gesamte Programm — selbst wenn main einen recover im defer hat.
package main
import (
"fmt"
"time"
)
func main() {
// Dieser recover fängt NICHTS aus der Goroutine unten.
defer func() {
if r := recover(); r != nil {
fmt.Println("main recover:", r) // läuft nicht
}
}()
go func() {
panic("aus Goroutine")
}()
time.Sleep(100 * time.Millisecond)
fmt.Println("main lebt noch")
}Wer Goroutinen sauber schützen will, muss in jeder einzelnen Goroutine ein eigenes defer ... recover() aufsetzen. Das ist die Server-Pattern-Variante aus Effective Go (Worker-Funktion mit eigenem Recover-Block), und der Stoff für den nächsten Artikel zu defer/recover-Pattern in Goroutinen.
Praxis 1 — Library-Boundary mit panic-zu-error
Eine Library, die intern rekursive Walk-Algorithmen ausführt, will den Fehler-Pfad nicht durch jeden Frame fädeln. Idiomatisch: intern panic benutzen, am Entry-Point per defer/recover in error konvertieren. Genau diese Strategie verfolgen text/template, encoding/json und der regexp-Compiler im Inneren.
package main
import (
"errors"
"fmt"
)
// Tree-Knoten, der per Walk verarbeitet wird.
type Node struct {
Name string
Value int
Children []*Node
}
// walkError ist ein interner Typ, den NUR diese Library wirft.
// Damit kann recover unterscheiden zwischen erwarteten Walk-Fehlern
// und unerwarteten Bugs.
type walkError struct {
Path string
Err error
}
func (w *walkError) Error() string {
return fmt.Sprintf("walk %q: %v", w.Path, w.Err)
}
func walk(n *Node, path string) int {
if n.Value < 0 {
// intern panic — kein error-Return durch alle Frames
panic(&walkError{Path: path, Err: errors.New("negativer Wert")})
}
sum := n.Value
for _, c := range n.Children {
sum += walk(c, path+"/"+c.Name)
}
return sum
}
// Sum ist der EXPORTED Entry-Point. Hier wird recover'd und zu error.
func Sum(root *Node) (total int, err error) {
defer func() {
switch r := recover().(type) {
case nil:
// alles gut
case *walkError:
// erwarteter interner Panic — als error zurück
err = r
default:
// unerwarteter Panic — weiterwerfen!
// Bug in der Library, soll nicht stillschweigend
// als „normaler" Fehler erscheinen.
panic(r)
}
}()
return walk(root, root.Name), nil
}
func main() {
tree := &Node{
Name: "root",
Value: 1,
Children: []*Node{
{Name: "a", Value: 2},
{Name: "b", Value: -5, Children: []*Node{
{Name: "c", Value: 3},
}},
},
}
total, err := Sum(tree)
if err != nil {
fmt.Println("Fehler:", err)
return
}
fmt.Println("Summe:", total)
}Fehler: walk "root/b": negativer WertBeobachtungen:
- Der
walkError-Typ ist die Eintrittskarte. Nur Panics dieses Typs werden „akzeptiert" und in error übersetzt. Alles andere wird mitpanic(r)weitergeworfen — sonst würde ein echter Bug (z. B. Nil-Deref) als „walk error" maskiert. - Der Entry-Point hat den Named Return
(total int, err error), damit der defer-Block den error setzen kann. - Aufrufer der Library sehen nie den panic-Mechanismus. Für sie ist
Sumeine ganz normale(value, error)-Funktion.
Praxis 2 — JSON-Decoder-Wrapper mit Stack-Logging
Ein Service nimmt JSON-Payloads aus untrusted Quellen entgegen. Bestimmte pathologische Inputs (sehr tief verschachtelt, bewusst kaputt formatiert, Interaktion mit Custom-Unmarshalern) können in seltenen Fällen aus dem encoding/json-Reflection-Code einen Panic auslösen. Wir wollen den Service nicht crashen lassen — aber wir wollen den Bug sehen.
package main
import (
"encoding/json"
"errors"
"fmt"
"log"
"runtime/debug"
)
// SafeDecode wickelt json.Unmarshal so, dass ein etwaiger panic
// als error zurückkommt UND ein Stack-Trace geloggt wird.
// So weiß Ops, dass es einen Bug zu fixen gibt — der Service
// bleibt aber für andere Requests am Leben.
func SafeDecode(data []byte, v any) (err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("json-decode panic: %v\n%s", r, debug.Stack())
err = fmt.Errorf("decode-panic: %v", r)
}
}()
if err := json.Unmarshal(data, v); err != nil {
return fmt.Errorf("ungültige JSON-Eingabe: %w", err)
}
return nil
}
// Custom-Type mit fehlerhaftem UnmarshalJSON, der einen panic auslöst.
type Risky struct {
Name string
}
func (r *Risky) UnmarshalJSON(b []byte) error {
// simulieren, was bei einem echten Reflection-Bug passieren würde
panic(errors.New("interner Decoder-Crash"))
}
func main() {
var out Risky
err := SafeDecode([]byte(`{"name":"test"}`), &out)
if err != nil {
fmt.Println("API gibt zurück:", err)
return
}
fmt.Println("OK:", out)
}API gibt zurück: decode-panic: interner Decoder-CrashWas dieser Wrapper richtig macht:
- Logging mit
debug.Stack()— das Ops-Team sieht den vollständigen Stack-Trace und kann den Bug rekonstruieren. - Konvertierung zu
error— der HTTP-Handler oben darüber kriegt ein normaleserror-Objekt und kann z. B.500 Internal Server Errorzurückgeben. - Service bleibt am Leben — andere Requests in anderen Goroutinen sind nicht betroffen, weil dieser
recoverin der request-lokalen Goroutine läuft. - Keine Falsch-Maskierung — falls der Panic ein
runtime.Errorist (Nil-Deref, Index out of bounds), bekommt der Aufrufer ihn als generischen Wrap zurück. Wer schärfer trennen will, baut denswitch r := recover().(type)-Block aus Praxis 1 ein.
Häufige Stolperfallen
panic ist KEIN Ersatz für error.
Wer panic für normale Fehler-Pfade einsetzt (fehlende Datei, Validierungsfehler, Netzwerk-Timeout), schreibt unidiomatischen Go-Code. Die Go-Library-Konvention ist klar: an der externen API werden Fehler als error-Rückgabewert kommuniziert. Panic ist für Programmierfehler und „kann nicht passieren"-Pfade reserviert.
recover muss DIREKT in der deferred-Funktion stehen.
defer recoverHelper() mit recover innerhalb von recoverHelper funktioniert nicht — die Spec verlangt „inside a deferred function (but not any function called by it)". Das kanonische Pattern ist die anonyme Closure defer func() { if r := recover(); ... }() ohne Indirektion.
recover fängt nicht über Goroutinen-Grenzen.
Ein recover in main fängt keine Panics aus Goroutinen, die main per go ... gestartet hat. Jede Goroutine braucht ihr eigenes defer/recover — sonst reißt ein einzelner Goroutinen-Panic das gesamte Programm runter, egal wie sorgfältig die anderen Stellen geschützt sind.
Named Return Values sind beim panic-zu-error-Pattern Pflicht.
Ohne func f() (err error) mit benanntem Rückgabewert kann die deferred-Funktion den error nicht mehr setzen — der Aufrufer bekommt den Zero-Value nil zurück, und der Panic verschwindet stillschweigend. Das ist eine der häufigsten Code-Review-Fundstellen rund um recover.
Manche Panics sind nicht recoverbar.
concurrent map writes, Stack-Overflow und einige Out-of-Memory-Situationen werden von der Runtime als fatal error eingestuft. Sie laufen defer nicht ab, recover fängt sie nicht. Wer einen Service gegen Map-Race schützen will, braucht sync.Mutex oder sync.Map — kein recover.
Recover ohne Re-Panic versteckt Bugs.
Wer alles auffängt und stillschweigend zu nil macht, hat einen Service, der „läuft" — aber mit kaputtem Zustand. Idiomatisch ist, fremde Panic-Typen weiterzuwerfen (panic(r)) und nur die erwarteten eigenen Typen als error zu übersetzen. Sonst ist der nächste Bug-Report eine Geisterjagd.
panic(nil) ist seit Go 1.21 selbst ein Laufzeitfehler.
Vorher konnte recover nicht unterscheiden zwischen „kein Panic" und „mit nil paniciert" — beide gaben nil zurück. Seit Go 1.21 ersetzt die Runtime panic(nil) durch einen typisierten *runtime.PanicNilError. Wer auf altes Verhalten angewiesen ist, kann GODEBUG=panicnil=1 setzen — sollte aber besser den Code reparieren.
os.Exit umgeht alle defers — auch in recover-Blocks.
os.Exit(1) terminiert das Programm sofort, ohne defer-Stacks abzuarbeiten. Wer in einem recover-Block os.Exit aufruft, verliert alle pending Defers (Datei-Closes, Mutex-Unlocks, Buffer-Flushes). Für „crashen mit Code" ist der ehrlichere Weg, den panic nicht zu recovern und die Runtime das Aufräumen machen zu lassen.
Weiterführende Ressourcen
Externe Quellen
- Handling panics — Go Language Specification
- Defer, Panic, and Recover — The Go Blog
- Effective Go: Recover
panic— Go builtin packagerecover— Go builtin packageruntime/debug.Stack— Stack-Trace-Erfassung- GOTRACEBACK — Runtime-Environment-Variable