Menu

Golang defer: Reihenfolge, Auswertung der Argumente, Fallstricke

defer plant einen Aufruf ein, der läuft, wenn die umgebende Funktion zurückkehrt. LIFO-Reihenfolge, wann Argumente ausgewertet werden, Dateien schließen und Mutexe entsperren, defer in Schleifen und benannte Ergebnisse ändern.

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

Was defer macht

defer legt einen Funktionsaufruf auf eine Liste. Wenn die umgebende Funktion zurückkehrt, wird die Liste in umgekehrter Reihenfolge abgearbeitet.

Ausgabe:

start
end
deferred 3
deferred 2
deferred 1

Die Reihenfolge ist Last in, first out, wie bei einem Stack. Das passt dazu, wie Ressourcen verschachtelt sind: Öffnest du A und dann B, willst du meist B vor A schließen.

Aufräumen direkt neben dem Belegen

Der Haupteinsatz von defer ist, das Aufräumen in die Zeile direkt nach dem Belegen zu schreiben, damit kein Return-Pfad es vergessen kann.

Achte auf die Reihenfolge: zuerst den Fehler prüfen, dann defer. Wenn os.Open fehlgeschlagen ist, ist f nil, und ein f.Close() per defer vor der Prüfung würde Close auf einem nil-*os.File aufrufen (das liefert einen Fehler, den du nie siehst, und führt Leser in die Irre).

Dieselbe Form funktioniert bei Locks:

mu.Lock()
defer mu.Unlock()

Löst der Code dazwischen eine Panic aus, wird der Mutex trotzdem entsperrt.

Argumente werden sofort ausgewertet

Die defer-Funktion und ihre Argumente werden ausgewertet, wenn die defer-Anweisung läuft. Nur der Aufruf selbst wartet.

Ausgabe:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) hat in der defer-Zeile den Wert 1 festgehalten. Die Closure hat keine Argumente; sie liest x, wenn sie schließlich läuft. Wähl die Form, die zu dem passt, was du festhalten willst.

Das gilt auch für Method Receiver. defer t.Stop() wertet t sofort aus, also ändert eine spätere Neuzuweisung von t nichts daran, welcher Wert gestoppt wird.

Ein verbreiteter Trick zur Zeitmessung nutzt diese Regel absichtlich:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") wird sofort aufgerufen (es kann „enter“ ausgeben und die Startzeit festhalten), und die Funktion, die es zurückgibt, wird per defer eingeplant.

defer in einer Schleife

Per defer eingeplante Aufrufe laufen, wenn die Funktion zurückkehrt, nicht am Ende jedes Schleifendurchlaufs. In einer Schleife über viele Dateien bleibt so jede Datei bis zum Ende der Funktion offen.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

Bei Tausenden von Pfaden gehen dir damit die File Descriptors aus. Verschieb den Rumpf in eine eigene Funktion, damit jedes defer pro Durchlauf läuft:

Jeder Aufruf von processFile schließt seine Datei, bevor die nächste geöffnet wird. Ein an Ort und Stelle aufgerufenes Funktionsliteral (func() { ... }()) funktioniert genauso, wenn dir eine benannte Hilfsfunktion zu viel ist.

Rückgabewerte ändern

Eine per defer eingeplante Closure läuft, nachdem die return-Anweisung die Ergebnisse zugewiesen hat, und kann benannte Ergebnisse ändern, bevor der Aufrufer sie sieht.

Das gibt 10 und save failed: disk full aus. Bei einem unbenannten Ergebnis kann eine defer-Funktion zwar laufen, hat aber keine Möglichkeit, den Rückgabewert zu ändern.

Den Fehler von Close festhalten

defer f.Close() wirft den Fehler von Close weg. Bei Dateien, die du nur liest, ist das in Ordnung. Bei Dateien, die du geschrieben hast, kann Close einen fehlgeschlagenen letzten Flush melden, also zählt der Fehler. Mit einem benannten Ergebnis behältst du ihn:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) ist eine kürzere Alternative, wenn du beide Fehler melden willst.

defer, panic und recover

Per defer eingeplante Aufrufe laufen, während eine Panic den Stack abbaut. Das ist der einzige Ort, an dem recover etwas bewirkt, und so verhindert ein Server, dass ein fehlerhafter Request den ganzen Prozess beendet. Die Details stehen unter panic und recover.

Per defer eingeplante Aufrufe laufen nicht, wenn das Programm über os.Exit oder log.Fatal beendet wird. Plant main Aufräumarbeiten per defer ein und ruft dann os.Exit(1) auf, wird das Aufräumen übersprungen.

Kosten

Seit Go 1.14 werden die meisten defers vom Compiler inline erzeugt (open-coded) und kosten ein paar Nanosekunden. defer für jede Mutex-Freigabe und jedes Schließen einer Datei zu nutzen ist der normale Stil. Die Ausnahme sind defers in Schleifen: Die können nicht open-coded werden und fallen auf einen langsameren Pfad zurück. Ein Grund mehr, Schleifenrümpfe in Funktionen auszulagern.

Häufige Fehler

  • defer vor der Fehlerprüfung. Prüf zuerst err von Open, dann defer Close.
  • Aufräumen pro Durchlauf in einer Schleife erwarten. defers laufen beim Verlassen der Funktion.
  • Erwarten, dass ein defer-Argument spätere Änderungen sieht. Argumente werden in der defer-Zeile festgelegt. Nimm eine Closure, um beim Verlassen zu lesen.
  • Sich bei os.Exit auf defer verlassen. Es läuft nie.

Häufig gestellte Fragen

Was macht defer in Go?

defer f() plant f() so ein, dass es läuft, wenn die umgebende Funktion zurückkehrt, egal ob normal, über ein frühes return oder durch eine Panic. Man nutzt es, um Aufräumcode (eine Datei schließen, einen Mutex entsperren) direkt neben den Code zu setzen, der die Ressource belegt hat.

In welcher Reihenfolge laufen per defer eingeplante Aufrufe in Go?

Last in, first out. Der zuletzt eingeplante Aufruf läuft zuerst. defer fmt.Println(1); defer fmt.Println(2) gibt 2 und dann 1 aus.

Wann werden die Argumente einer defer-Funktion ausgewertet?

Sofort, wenn die defer-Anweisung ausgeführt wird, nicht wenn der Aufruf läuft. x := 1; defer fmt.Println(x); x = 2 gibt 1 aus. Um den Wert zum Zeitpunkt des Verlassens zu lesen, plane eine Closure ein: defer func() { fmt.Println(x) }().

Läuft defer bei einer Panic oder bei os.Exit?

Per defer eingeplante Aufrufe laufen, während eine Panic den Stack abbaut. Deshalb funktioniert recover darin. Sie laufen nicht, wenn das Programm os.Exit aufruft (oder log.Fatal, das es aufruft), und sie laufen nicht in anderen Goroutinen, wenn main zurückkehrt.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S