Menu

defer in Golang: ordine, valutazione degli argomenti e insidie

defer pianifica una chiamata da eseguire quando la funzione che la contiene ritorna. Scopri l'ordine LIFO, quando vengono valutati gli argomenti, come chiudere file e sbloccare mutex, defer nei cicli e come modificare i risultati con nome.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Cosa fa defer

defer aggiunge una chiamata di funzione a una lista. Quando la funzione che la contiene ritorna, la lista viene eseguita in ordine inverso.

Output:

start
end
deferred 3
deferred 2
deferred 1

L'ordine è LIFO (l'ultimo inserito è il primo a uscire), come in uno stack. Corrisponde al modo in cui le risorse si annidano: se apri A e poi B, di solito vuoi chiudere B prima di A.

La pulizia accanto all'acquisizione

L'uso principale di defer è scrivere la pulizia nella riga subito dopo l'acquisizione, così nessun percorso di uscita può dimenticarla.

Nota l'ordine: prima controlla l'errore, poi usa defer. Se os.Open fallisce, f è nil e differire f.Close() prima del controllo chiamerebbe Close su un *os.File nil (che restituisce un errore che non vedi mai e confonde chi legge il codice).

Lo stesso schema funziona per i lock:

mu.Lock()
defer mu.Unlock()

Se il codice tra le due righe va in panic, il mutex viene comunque sbloccato.

Gli argomenti vengono valutati subito

La funzione differita e i suoi argomenti vengono valutati quando viene eseguita l'istruzione defer. Solo la chiamata vera e propria aspetta.

Output:

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

fmt.Println("...", x) ha catturato il valore 1 alla riga del defer. La closure non ha argomenti; legge x quando finalmente viene eseguita. Scegli la forma che corrisponde a ciò che vuoi registrare.

Questo vale anche per i receiver dei metodi. defer t.Stop() valuta t subito, quindi riassegnare t in seguito non cambia quale valore viene fermato.

Un classico trucco per misurare i tempi sfrutta questa regola di proposito:

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

trace("handle") viene chiamata subito (può stampare "enter" e registrare l'orario di inizio), ed è la funzione che restituisce a essere differita.

defer in un ciclo

Le chiamate differite vengono eseguite quando la funzione ritorna, non alla fine di ogni iterazione del ciclo. In un ciclo su molti file, questo tiene aperto ogni file fino alla fine della funzione.

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)
}

Con migliaia di percorsi, finisci i descrittori di file. Sposta il corpo in una funzione a parte, così ogni defer viene eseguito a ogni iterazione:

Ogni chiamata a processFile chiude il suo file prima che si apra il successivo. Un letterale di funzione chiamato sul posto (func() { ... }()) funziona allo stesso modo quando una funzione di supporto con nome ti sembra eccessiva.

Modificare i valori di ritorno

Una closure differita viene eseguita dopo che l'istruzione return ha assegnato i risultati, e può modificare i risultati con nome prima che il chiamante li veda.

Stampa 10 e save failed: disk full. Con un risultato senza nome, una funzione differita viene comunque eseguita, ma non ha modo di cambiare ciò che viene restituito.

Catturare l'errore di Close

defer f.Close() scarta l'errore di Close. Per i file che leggi soltanto va bene. Per i file che hai scritto, Close può segnalare il fallimento dell'ultima scrittura su disco, quindi l'errore conta. Un risultato con nome ti permette di conservarlo:

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()) è un'alternativa più breve quando vuoi segnalare entrambi gli errori.

defer, panic e recover

Le chiamate differite vengono eseguite mentre un panic risale lo stack. È l'unico punto in cui recover ha effetto, ed è il modo in cui un server evita che una richiesta difettosa faccia cadere l'intero processo. I dettagli sono nella pagina su panic e recover.

Le chiamate differite non vengono eseguite quando il programma termina tramite os.Exit o log.Fatal. Se main differisce una pulizia e poi chiama os.Exit(1), la pulizia viene saltata.

Costo

Da Go 1.14, la maggior parte dei defer viene espansa inline dal compilatore e costa pochi nanosecondi. Usare defer per ogni rilascio di mutex e chiusura di file è lo stile normale. I defer dentro i cicli sono l'eccezione: non possono essere espansi inline e ripiegano su un percorso più lento, un motivo in più per spostare il corpo dei cicli in funzioni separate.

Errori comuni

  • Usare defer prima del controllo dell'errore. Controlla prima l'err di Open, poi defer Close.
  • Aspettarsi una pulizia a ogni iterazione in un ciclo. I defer vengono eseguiti all'uscita dalla funzione.
  • Aspettarsi che un argomento differito veda le modifiche successive. Gli argomenti sono fissati alla riga del defer. Usa una closure per leggerli all'uscita.
  • Contare su defer con os.Exit. Non viene mai eseguito.

Domande frequenti

Cosa fa defer in Go?

defer f() pianifica l'esecuzione di f() quando la funzione che la contiene ritorna, sia che ritorni normalmente, con un return anticipato o a causa di un panic. Si usa per mettere la pulizia (chiudere un file, sbloccare un mutex) proprio accanto al codice che ha acquisito la risorsa.

In che ordine vengono eseguite le chiamate differite in Go?

L'ultima inserita è la prima a uscire. La chiamata differita più recente viene eseguita per prima. defer fmt.Println(1); defer fmt.Println(2) stampa 2 e poi 1.

Quando vengono valutati gli argomenti di una funzione differita?

Subito, quando viene eseguita l'istruzione defer, non quando parte la chiamata. x := 1; defer fmt.Println(x); x = 2 stampa 1. Per leggere il valore al momento dell'uscita, differisci una closure: defer func() { fmt.Println(x) }().

defer viene eseguito in caso di panic o di os.Exit?

Le chiamate differite vengono eseguite mentre un panic risale lo stack, ed è per questo che recover funziona al loro interno. Non vengono eseguite quando il programma chiama os.Exit (o log.Fatal, che lo chiama), e non vengono eseguite nelle altre goroutine quando main ritorna.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA