Andere Sprachen haben try/finally, using, Destruktoren oder Context-Manager — Go hat defer. Ein einziges Keyword, das einen Funktionsaufruf bis zum Ende der umgebenden Funktion verschiebt, garantiert, auch wenn ein Panic dazwischenkommt. Das macht defer zur sauberen, lesbaren Antwort auf das uralte Problem: Wer schließt das geöffnete File? Wer gibt das Mutex frei? Wer macht das Rollback? Die Antwort lautet immer „die deferred Funktion am Anfang". Diese Eleganz hat aber drei Eigenheiten, die in echtem Code regelmäßig zuschlagen: Argumente werden bei defer ausgewertet, nicht bei der Ausführung. Mehrere defers feuern in LIFO-Reihenfolge. Und defer in einer Schleife sammelt sich an, bis die ganze Funktion fertig ist. Dieser Artikel arbeitet die Mechanik formal durch und zeigt jede Falle mit lauffähigen Beispielen.
Grundmechanik — was defer eigentlich tut
Ein defer-Statement registriert einen Funktionsaufruf, der direkt vor dem Return der umgebenden Funktion ausgeführt wird. Egal, wie die Funktion endet — normaler Return, mehrere Return-Pfade, oder ein Panic, der den Stack hochläuft: die deferred Aufrufe laufen.
DeferStmt = "defer" Expression .Der Ausdruck nach defer muss ein Funktionsaufruf sein (Call-Expression), kein beliebiger Ausdruck. defer x + 1 ist Syntax-Fehler. Erlaubt sind: benannte Funktion, Methode, Closure, Interface-Methodenaufruf — alles, was eine Call-Expression ergibt.
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("Ende — über defer")
fmt.Println("Mitte")
// Hier endet die Funktion. Erst jetzt läuft das defer.
}Start
Mitte
Ende — über deferDrei Eigenschaften, die diese Mechanik ausmachen:
- Registrierung sofort, Ausführung später.
deferlegt den Aufruf auf einen funktions-lokalen Stack — die eigentliche Ausführung passiert erst beim Return. - Garantiert beim normalen Return und bei Panic. Wenn der Stack durch einen Panic hochläuft, werden alle bisher registrierten defers in jeder Funktion auf dem Weg nach oben gefeuert. Das ist die Basis für
recover. - Reihenfolge: LIFO. Mehrere defers feuern in umgekehrter Reihenfolge der Registrierung — das letzte registrierte zuerst.
Die Go-Spec formuliert das knapp: „Deferred function calls are executed in Last-In-First-Out order after the surrounding function returns."
Argument-Capture — die berüchtigte Falle
Die erste der drei klassischen defer-Regeln aus dem Go-Blog: „A deferred function's arguments are evaluated when the defer statement is evaluated." Die Argumente des deferred Calls werden sofort ausgewertet — der Aufruf selbst läuft erst später. Klingt subtil, ist aber die häufigste defer-Falle überhaupt.
package main
import "fmt"
func main() {
i := 0
defer fmt.Println("deferred:", i) // i wird HIER ausgewertet — als 0
i++
fmt.Println("direkt:", i) // 1
// Beim Return läuft das defer mit dem damals festgehaltenen Wert 0.
}direkt: 1
deferred: 0Der Trick: fmt.Println ist eine Funktion mit zwei Argumenten. Beide werden bei defer ausgewertet — das i darin ist also der Wert von i zum Zeitpunkt des defer-Statements, nicht zum Zeitpunkt der späteren Ausführung. Es ist als hätte man arg := i; defer fmt.Println("deferred:", arg) geschrieben.
Wer das spätere i lesen will, muss eine Closure verwenden — denn dann ist es die Closure-Funktion, die deferred ist, und die liest i aus dem umgebenden Scope:
package main
import "fmt"
func main() {
i := 0
defer func() {
// Closure liest i zum AUSFÜHRUNGS-Zeitpunkt
fmt.Println("deferred (Closure):", i)
}()
i++
fmt.Println("direkt:", i) // 1
}direkt: 1
deferred (Closure): 1Der semantische Unterschied ist subtil, aber wichtig:
| Form | Was wird eingefroren? | Wann liest die Variable? |
|---|---|---|
defer fmt.Println(i) | Argument-Wert i zum defer-Zeitpunkt | Wert ist eingefroren |
defer func() { fmt.Println(i) }() | Funktions-Closure (Referenz auf i) | Lesen zum Ausführungs-Zeitpunkt |
Beides hat seinen Platz. Die Argument-Form ist meistens das, was du beim Cleanup willst — der Filehandle, den du jetzt schließen willst, ist exakt der, den du jetzt geöffnet hast. Die Closure-Form brauchst du, wenn der Wert sich noch ändert und du den finalen Stand sehen willst (Logging am Funktions-Ende, Error-Wrapping bei Named Returns).
LIFO-Reihenfolge — der defer-Stack
Mehrere defer-Statements in einer Funktion sind ein Stack: das zuletzt registrierte feuert zuerst, das zuerst registrierte zuletzt. Die Go-Spec formuliert es als Last-In-First-Out — exakt wie ein klassischer Push/Pop-Stack.
package main
import "fmt"
func main() {
fmt.Println("Start")
defer fmt.Println("defer 1 (erstes registriert, läuft zuletzt)")
defer fmt.Println("defer 2")
defer fmt.Println("defer 3 (letztes registriert, läuft zuerst)")
fmt.Println("Ende des Bodys")
}Start
Ende des Bodys
defer 3 (letztes registriert, läuft zuerst)
defer 2
defer 1 (erstes registriert, läuft zuletzt)Die LIFO-Reihenfolge ist nicht beliebig, sondern semantisch wichtig: Cleanup-Operationen müssen oft in umgekehrter Reihenfolge der Acquisition laufen. Wer erst die Datenbank öffnet, dann eine Transaktion startet, will am Ende erst die Transaktion abschließen, dann die Verbindung schließen. Mit defer schreibt sich das auf natürliche Weise:
func processOrder(orderID string) error {
db, err := openDB()
if err != nil {
return err
}
defer db.Close() // (1) registriert, läuft als Letztes
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // (2) registriert, läuft vor db.Close()
// ... Arbeit ...
return tx.Commit()
// LIFO: Rollback wird gerufen (no-op, wenn Commit schon lief), dann db.Close()
}Beachte das Pattern „defer tx.Rollback() direkt nach db.Begin()". Das Rollback() ist nach einem erfolgreichen Commit() ein No-Op (Standard-database/sql-Verhalten) — es schadet also nicht, wenn der Happy Path durchläuft. Bei einem Fehler oder Panic rettet es aber den Tag.
Typische Use-Cases — wofür defer wirklich da ist
defer ist in Go der idiomatische Cleanup-Mechanismus. Vier Muster, die in jedem nicht-trivialen Programm vorkommen:
package main
import (
"io"
"os"
"sync"
)
// (1) Datei schließen
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // direkt nach erfolgreichem Open
return io.ReadAll(f)
}
// (2) Mutex freigeben
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock() // direkt nach Lock
c.value++
// Auch wenn hier ein Panic auftritt, bleibt der Mutex nicht hängen.
}
// (3) DB-Transaktion
func transfer(db *sql.DB, from, to string, amount int) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // no-op nach erfolgreichem Commit
// ... Updates ...
return tx.Commit()
}
// (4) Timer / Tracing
func slowOp() {
defer trace("slowOp")() // siehe nächster Abschnitt
// ... Arbeit ...
}Das Anti-Pattern, das man oft sieht: defer erst irgendwo mitten in der Funktion, statt direkt nach der Resource-Acquisition. Wenn zwischen Open() und defer Close() noch eine Verzweigung mit return kommt, leakt die Resource bei diesem Pfad. Die Faustregel ist hart: defer gehört in die nächste Zeile nach der Acquisition.
defer + Named Returns — den Return-Wert verändern
Die dritte defer-Regel aus dem Go-Blog: „Deferred functions may read and assign to the returning function's named return values." Wenn eine Funktion benannte Return-Werte hat, sieht eine deferred Closure diese Variablen — und darf sie modifizieren. Das passiert nach dem return-Statement, aber vor dem Verlassen des Funktions-Frames.
package main
import "fmt"
// Benannter Return-Wert: i
func plusOne() (i int) {
defer func() {
i++ // modifiziert i NACH dem return, vor dem Verlassen
}()
return 1 // setzt i auf 1, dann läuft defer, dann ist der echte Return-Wert 2
}
func main() {
fmt.Println(plusOne()) // 2, nicht 1
}2Wie das mechanisch abläuft:
return 1weistiden Wert1zu (weilider benannte Return ist).- Das deferred Closure läuft und macht
i++—iist jetzt2. - Die Funktion verlässt den Frame mit
i == 2.
Das ist kein Bug, sondern eine bewusste Sprach-Eigenschaft. Der nützlichste Use-Case: Error-Wrapping am Funktions-Ende, ohne den Happy Path zu verschmutzen:
package main
import (
"errors"
"fmt"
"strconv"
)
// Wickelt jeden Fehler einheitlich in einen Kontext-String.
func parseAge(s string) (age int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("parseAge(%q): %w", s, err)
}
}()
if s == "" {
return 0, errors.New("leer")
}
return strconv.Atoi(s)
}
func main() {
_, err := parseAge("abc")
fmt.Println(err)
}parseAge("abc"): strconv.Atoi: parsing "abc": invalid syntaxEleganter als das manuelle Wrappen an jedem einzelnen Error-Return — der Wrapper steht einmal oben, und alle Pfade laufen automatisch durch ihn.
Wichtig: Das funktioniert nur mit benannten Returns. Bei func foo() error ohne Namen kann die deferred Closure den Return-Wert nicht erreichen — sie sieht nur den lokalen Scope.
defer mit Closure — Argument-Capture umgehen
Wir haben den Closure-Trick schon im Argument-Capture-Abschnitt gesehen. Hier kommt der elegante Anwendungsfall: ein Timer, der bei Funktions-Start startet und bei Funktions-Ende loggt.
package main
import (
"fmt"
"time"
)
// trace gibt eine Funktion zurück, die beim Aufruf die Dauer loggt.
func trace(name string) func() {
start := time.Now()
fmt.Printf("→ %s gestartet\n", name)
return func() {
fmt.Printf("← %s fertig in %v\n", name, time.Since(start))
}
}
func slowOp() {
// Das äußere trace("slowOp") läuft sofort und gibt eine Closure zurück.
// Diese Closure wird mit defer verschoben — feuert beim Funktions-Ende.
defer trace("slowOp")()
time.Sleep(50 * time.Millisecond)
}
func main() {
slowOp()
}→ slowOp gestartet
← slowOp fertig in 50.xxxmsDas () am Ende der defer-Zeile ist entscheidend: defer trace("slowOp")() heißt „rufe trace("slowOp") jetzt auf, das gibt eine Funktion zurück, und genau diese Funktion wird deferred". Ohne das letzte () würdest du trace("slowOp") deferren, was funktional dasselbe Ergebnis hätte — aber dann läuft der Print am Anfang gar nicht jetzt, sondern erst am Ende.
Die Faustregel: Argument-Capture nutzen, wenn du den Wert zum defer-Zeitpunkt einfrieren willst. Closure nutzen, wenn du den Wert zum Ausführungs-Zeitpunkt sehen willst.
defer in Schleifen — das Resource-Leak
Die zweite große Falle: defer in einer for-Schleife. Jeder Schleifen-Durchlauf registriert ein neues defer, das erst am Funktions-Ende feuert — nicht am Ende des Schleifen-Bodys. Wer 10.000 Dateien in einer Schleife öffnet, hat 10.000 offene Filehandles, bis die ganze Funktion durchläuft.
package main
import (
"io"
"os"
)
// ANTI-PATTERN: alle Dateien bleiben bis zum Funktions-Ende offen
func processAllBad(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // sammelt sich an — feuert erst nach der Schleife
_, _ = io.ReadAll(f)
}
return nil
// Wenn paths 10.000 Einträge hat, sind 10.000 offene Filehandles
// gleichzeitig im OS — Limit-Überschreitung droht.
}Die saubere Lösung: den Schleifen-Body in eine eigene Funktion auslagern. Dann gilt das defer für diese Funktion und feuert sauber pro Iteration:
package main
import (
"io"
"os"
)
func processOne(p string) error {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // feuert beim Verlassen von processOne — also pro Iteration
_, err = io.ReadAll(f)
return err
}
func processAllGood(paths []string) error {
for _, p := range paths {
if err := processOne(p); err != nil {
return err
}
}
return nil
}Alternative ohne Helper-Funktion: eine anonyme Funktion direkt in der Schleife, die als eigener Scope dient. Funktional gleichwertig, aber meist weniger lesbar:
for _, p := range paths {
func() {
f, err := os.Open(p)
if err != nil {
return
}
defer f.Close() // an die anonyme Funktion gebunden
_, _ = io.ReadAll(f)
}()
}Wer eine Codebase nach defer in for-Schleifen scannt, findet fast immer einen latenten Leak. Linter wie golangci-lint haben dafür eigene Checks (gocheckcompilerdirectives-nahe Regeln, oder wastedassign/gocritic-Sets).
defer + panic/recover — der Rettungsanker
recover funktioniert ausschließlich innerhalb einer deferred Funktion. Das ist nicht beliebig: Wenn der Stack durch einen Panic hochläuft, ist der einzige Code, der noch garantiert läuft, der deferred Code in den Frames auf dem Weg nach oben. Genau dort fängt recover den Panic ab und gibt ihn als Wert zurück.
package main
import "fmt"
func safe() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
panic("etwas ist schiefgegangen")
}
func main() {
err := safe()
fmt.Println(err)
fmt.Println("Programm läuft weiter.")
}recovered: etwas ist schiefgegangen
Programm läuft weiter.Drei Regeln, die hier mitspielen:
recover()außerhalb eines deferred Calls liefertnil. Es passiert nichts, der Panic läuft weiter.recover()wirkt nur für Panics im selben Funktions-Frame. Wennsafe()eine andere Funktion aufruft, die paniciert, und dort ein deferred recover steht, wird der Panic dort gefangen — nicht bei uns.- Nach einem erfolgreichen
recover()ist der Panic „gestoppt". Die Funktion kehrt normal zurück (mit den Werten, die die deferred Funktion gesetzt hat), die aufrufende Funktion läuft unverändert weiter.
recover ist kein Allzweck-Try/Catch. In idiomatischem Go wird es selten gebraucht — primär an Funktions-Grenzen, die kein Panic über die API-Grenze hinaus durchlassen dürfen (HTTP-Handler, RPC-Server, lange laufende Goroutines). Mehr dazu im Error-Handling-Kapitel.
defer-Performance — Overhead und Open-Coded Optimierung
defer ist nicht kostenlos. Jeder defer-Aufruf legt einen Eintrag auf den funktions-lokalen Stack, mit Argumenten, Funktions-Pointer und Verweis auf die nächste Eintragung. Beim Return läuft der Stack zurück. In den Jahren vor Go 1.13 waren das spürbare ~50–100 Nanosekunden pro defer — genug, dass die Standard-Library ihre sync.Mutex.Unlock()-Aufrufe an Hot-Paths bewusst ohne defer schrieb.
Seit Go 1.14 gibt es Open-Coded Defer: wenn der Compiler statisch erkennen kann, dass eine Funktion höchstens acht defers hat und keine davon in einer Schleife steht, inlinet er den Cleanup-Code direkt — der Overhead sinkt auf nahe null. Praktisch heißt das: in modernen Go-Programmen ist defer für 95 % aller Use-Cases performance-neutral.
package main
import "sync"
var mu sync.Mutex
// Variante A: idiomatisch — defer
func incA(v *int) {
mu.Lock()
defer mu.Unlock()
*v++
}
// Variante B: manuell — minimal schneller in Mikro-Benchmarks
func incB(v *int) {
mu.Lock()
*v++
mu.Unlock()
}
// Benchmark mit `go test -bench=.`:
// Auf modernem Go sind beide praktisch gleich schnell.
// Variante B verliert aber die Panic-Sicherheit.Faustregel: Schreibe defer, es sei denn, ein Profiler zeigt explizit, dass es der Engpass ist. Die Lesbarkeit und Panic-Sicherheit überwiegt fast immer den Mikro-Overhead. Open-Coded Defer schließt die historische Lücke — alte Optimierungs-Tipps, die defer aus Performance-Gründen ablehnen, sind veraltet.
Best Practices — wie defer in echter Codebase aussieht
Drei Regeln, die in praktisch jeder Go-Codebase gelten:
| Regel | Begründung |
|---|---|
defer direkt nach der Resource-Acquisition | Zwischen Acquisition und defer darf kein return liegen — sonst leakt die Resource auf dem frühen Pfad |
defer nicht in Schleifen auf direktem Pfad | Sammelt sich an bis zum Funktions-Ende — in eine eigene Funktion auslagern |
Errors aus defer Close() ggf. bewusst abfangen | f.Close() kann auch fehlschlagen (Schreib-Flush) — bei kritischen Writes prüfen |
Der letzte Punkt ist subtil: defer f.Close() wirft den Error des Close()-Aufrufs weg. Bei Read-Pfaden ist das meistens egal — beim Schreiben kann Close() den finalen Flush bringen, und ein Fehler dort heißt, die Datei ist nicht komplett geschrieben:
package main
import (
"errors"
"os"
)
// Schreibt mit korrekt propagiertem Close-Error.
// errors.Join (Go 1.20+) kombiniert beide Errors, falls beide auftreten.
func writeImportant(path string, data []byte) (err error) {
f, ferr := os.Create(path)
if ferr != nil {
return ferr
}
defer func() {
if cerr := f.Close(); cerr != nil {
err = errors.Join(err, cerr)
}
}()
_, err = f.Write(data)
return err
}Die deferred Closure nutzt den benannten Return err, prüft das Close()-Ergebnis, und kombiniert es mit dem ggf. schon gesetzten Schreib-Error. Wer ohne errors.Join auskommen muss, kann auch if err == nil { err = cerr } schreiben — der erste Fehler hat Priorität.
Häufige Stolperfallen
Argumente werden bei defer ausgewertet, nicht bei der Ausführung.
defer fmt.Println(i) friert i zum defer-Zeitpunkt ein. Wer den späteren Wert sehen will, braucht eine Closure: defer func() { fmt.Println(i) }(). Klassischer Bug in Schleifen, wo Loop-Variable und Iteration miteinander wandern, das defer-Argument aber den damaligen Wert behält.
defer in for-Loops sammelt sich an — Resource-Leak.
Jeder Schleifen-Durchgang registriert ein neues defer, das alle erst beim Funktions-Ende feuern. Bei 10.000 Iterationen sind 10.000 offene Filehandles gleichzeitig offen. Lösung: Schleifen-Body in eine eigene Funktion auslagern, dann gilt defer pro Iteration.
os.Exit und log.Fatal umgehen defer komplett.
os.Exit(1) beendet den Prozess sofort — kein deferred Code läuft. log.Fatal* ruft intern os.Exit. Das heißt: offene Files werden vom OS aufgeräumt, aber mu.Unlock(), tx.Rollback() oder Buffer-Flushes laufen nicht. Wer ordentliches Cleanup will, muss vor os.Exit selbst aufräumen — oder den Error nach oben durchreichen.
LIFO-Reihenfolge überrascht bei mehreren defers.
Drei defer A(); defer B(); defer C() feuern als C, B, A — das letzte zuerst. Bei Cleanup-Stacks (DB öffnen → Transaktion starten) ist das genau richtig (Transaktion schließt zuerst, DB danach). Wer eine andere Reihenfolge will, muss die defer-Statements anders sortieren oder die Cleanup-Logik in eine eigene Closure verpacken.
defer auf einem nil-Receiver paniciert beim Call.
defer obj.Close() evaluiert die Method-Expression sofort — das obj selbst wird festgehalten. Wenn obj zu diesem Zeitpunkt nil ist und Close keinen nil-Receiver verträgt, paniciert es erst beim späteren Call. Heißt: der Panic kommt aus dem Cleanup-Pfad, nicht aus der eigentlichen Logik — Stack-Trace ist verwirrend. Vor dem defer prüfen: if obj != nil { defer obj.Close() }.
defer f.Close() ignoriert den Close-Error stillschweigend.
Bei Read-Pfaden meistens harmlos. Bei Schreib-Pfaden gefährlich — Close() kann den finalen Flush bringen, und ein Fehler dort heißt, die Datei ist unvollständig. Idiom: defer func() { err = errors.Join(err, f.Close()) }() mit benanntem Return err, dann landet der Close-Error wirklich beim Caller.
defer mit direktem Call vs. Closure ist semantisch unterschiedlich.
defer foo(x) evaluiert x jetzt und ruft foo später mit dem eingefrorenen Wert. defer func() { foo(x) }() ruft foo später mit dem dann aktuellen x. Wer die falsche Form wählt, bekommt entweder einen alten oder einen neuen Wert — beide Forms sehen oberflächlich gleich aus.
recover wirkt nur in der direkt aufrufenden Funktion.
Ein defer func() { recover() }() in main fängt nicht den Panic, der drei Funktionen tief in einem Helper auftritt — es sei denn, der Panic läuft den Stack ohne weiteren recover bis zum main-Frame hoch. Aber: recover in einer deferred Closure, die selbst eine andere Funktion aufruft, funktioniert nicht — recover muss direkt im deferred Code stehen.
Named Return + defer kann den Return mutieren — überraschend für Caller.
Wenn die Funktions-Signatur func foo() (err error) lautet, kann eine deferred Closure err noch nach dem return-Statement modifizieren. Eleganter Error-Wrapping-Trick, aber für Leser, die das Pattern nicht kennen, unsichtbare Magie. Im Doc-Kommentar erwähnen oder konsistent in der ganzen Codebase verwenden.
Weiterführende Ressourcen
Externe Quellen
- Defer statements – Go Language Specification
- Handling panics – Go Language Specification
- Defer, Panic, and Recover – The Go Blog
- Go 1.14 Release Notes: Open-Coded Defer
- Effective Go: Defer
Verwandte Artikel
- if – Init-Klausel, Scoping und comma-ok
- for – die einzige Loop-Form und ihre Varianten
- switch und type switch – Pattern-Matching auf Werten und Typen
- break, continue, goto – Sprünge in Kontrollstrukturen
- Funktionen – Übersicht über Signaturen, Returns und Closures
- Goroutines – nebenläufige Funktionen und Cleanup-Disziplin