Menu

Defer en Golang : ordre d'exécution, évaluation des arguments et pièges

defer programme un appel qui s'exécute au retour de la fonction englobante. Découvrez l'ordre LIFO, le moment où les arguments sont évalués, la fermeture de fichiers et le déverrouillage de mutex, defer dans les boucles, et la modification des résultats nommés.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Ce que fait defer

defer ajoute un appel de fonction à une liste. Quand la fonction englobante retourne, la liste s'exécute dans l'ordre inverse.

Sortie :

start
end
deferred 3
deferred 2
deferred 1

L'ordre est dernier entré, premier sorti, comme une pile. Cela correspond à la façon dont les ressources s'imbriquent : si vous ouvrez A puis B, vous voulez en général fermer B avant A.

Le nettoyage à côté de l'acquisition

L'usage principal de defer est d'écrire le nettoyage sur la ligne qui suit l'acquisition, pour qu'aucun chemin de retour ne puisse l'oublier.

Remarquez l'ordre : vérifiez d'abord l'erreur, puis différez. Si os.Open a échoué, f est nil, et différer f.Close() avant la vérification appellerait Close sur un *os.File nil (ce qui renvoie une erreur que vous ne voyez jamais, et induit le lecteur en erreur).

La même forme fonctionne pour les verrous :

mu.Lock()
defer mu.Unlock()

Si le code entre les deux provoque un panic, le mutex est quand même déverrouillé.

Les arguments sont évalués immédiatement

La fonction différée et ses arguments sont évalués quand l'instruction defer s'exécute. Seul l'appel lui-même attend.

Sortie :

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

fmt.Println("...", x) a capturé la valeur 1 à la ligne du defer. La closure n'a pas d'argument ; elle lit x quand elle s'exécute enfin. Choisissez la forme qui correspond à ce que vous voulez enregistrer.

Cela s'applique aussi aux receveurs de méthode. defer t.Stop() évalue t tout de suite, donc réaffecter t plus tard ne change pas la valeur qui sera arrêtée.

Une astuce courante de mesure du temps exploite volontairement cette règle :

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

trace("handle") est appelée immédiatement (elle peut afficher « enter » et noter l'heure de début), et c'est la fonction qu'elle renvoie qui est différée.

Defer dans une boucle

Les appels différés s'exécutent au retour de la fonction, pas à la fin de chaque itération. Dans une boucle sur de nombreux fichiers, cela garde tous les fichiers ouverts jusqu'à la fin de la fonction.

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

Avec des milliers de chemins, on finit par manquer de descripteurs de fichiers. Déplacez le corps dans sa propre fonction pour que chaque defer s'exécute à chaque itération :

Chaque appel à processFile ferme son fichier avant que le suivant ne s'ouvre. Un littéral de fonction appelé sur place (func() { ... }()) fonctionne de la même façon quand une fonction nommée semble superflue.

Modifier les valeurs de retour

Une closure différée s'exécute après que l'instruction return a affecté les résultats, et elle peut modifier les résultats nommés avant que l'appelant ne les voie.

Ce code affiche 10 et save failed: disk full. Avec un résultat non nommé, une fonction différée s'exécute quand même, mais elle n'a aucun moyen de changer ce qui est renvoyé.

Récupérer l'erreur de Close

defer f.Close() jette l'erreur de Close. Pour des fichiers que vous lisez seulement, c'est acceptable. Pour des fichiers que vous avez écrits, Close peut signaler un vidage final raté, donc l'erreur compte. Un résultat nommé permet de la garder :

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()) est une alternative plus courte quand vous voulez signaler les deux erreurs.

Defer, panic et recover

Les appels différés s'exécutent pendant qu'un panic déroule la pile. C'est le seul endroit où recover a un effet, et c'est ainsi qu'un serveur évite qu'une mauvaise requête tue le processus. Les détails sont sur la page panic et recover.

Les appels différés ne s'exécutent pas quand le programme se termine via os.Exit ou log.Fatal. Si main diffère un nettoyage puis appelle os.Exit(1), le nettoyage est sauté.

Coût

Depuis Go 1.14, la plupart des defers sont intégrés directement par le compilateur (open-coded) et coûtent quelques nanosecondes. Utiliser defer pour chaque libération de mutex et chaque fermeture de fichier est le style normal. Les defers dans les boucles sont l'exception : ils ne peuvent pas être intégrés ainsi et passent par un chemin plus lent, une raison de plus pour déplacer le corps des boucles dans des fonctions.

Erreurs courantes

  • Différer avant la vérification d'erreur. Vérifiez d'abord le err de Open, puis defer Close.
  • S'attendre à un nettoyage à chaque itération d'une boucle. Les defers s'exécutent à la sortie de la fonction.
  • S'attendre à ce qu'un argument différé voie les modifications ultérieures. Les arguments sont figés à la ligne du defer. Utilisez une closure pour lire à la sortie.
  • Compter sur defer avec os.Exit. Il ne s'exécute jamais.

Questions fréquentes

Que fait defer en Go ?

defer f() programme f() pour qu'elle s'exécute au retour de la fonction englobante, qu'elle retourne normalement, par un return anticipé ou par un panic. On l'utilise pour placer le nettoyage (fermer un fichier, déverrouiller un mutex) juste à côté du code qui a acquis la ressource.

Dans quel ordre s'exécutent les appels différés en Go ?

Dernier entré, premier sorti. L'appel différé le plus récent s'exécute en premier. defer fmt.Println(1); defer fmt.Println(2) affiche 2 puis 1.

Quand les arguments d'une fonction différée sont-ils évalués ?

Immédiatement, quand l'instruction defer s'exécute, pas quand l'appel a lieu. x := 1; defer fmt.Println(x); x = 2 affiche 1. Pour lire la valeur au moment de la sortie, différez une closure : defer func() { fmt.Println(x) }().

Defer s'exécute-t-il lors d'un panic ou d'un os.Exit ?

Les appels différés s'exécutent pendant qu'un panic déroule la pile, c'est pourquoi recover fonctionne à l'intérieur. Ils ne s'exécutent pas quand le programme appelle os.Exit (ou log.Fatal, qui l'appelle), et ils ne s'exécutent pas dans les autres goroutines quand main retourne.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER