Fonctions anonymes
Un littéral de fonction est une fonction sans nom. C'est une expression, il peut donc aller partout où va une valeur : une variable, un argument, une valeur de retour, un champ de struct.
Go n'a pas de raccourci flèche ou lambda. La forme complète func(params) results { body } est la seule façon d'en écrire un.
Capturer des variables
Un littéral de fonction peut utiliser les variables de la fonction englobante. Il ne les copie pas : la closure et le code environnant partagent une seule variable.
La variable capturée vit tant qu'une closure y fait encore référence, même après le retour de la fonction qui l'a déclarée. Le compilateur la déplace sur le tas si nécessaire ; vous ne gérez jamais cela vous-même.
Générateurs et compteurs
Renvoyer une closure depuis une fonction donne à chaque fonction renvoyée son propre état privé.
Chaque appel à counter() crée un nouveau n, donc c1 et c2 comptent indépendamment. Rien à l'extérieur ne peut lire ou réinitialiser n, ce qui en fait une petite forme d'encapsulation.
Closures dans les boucles (Go 1.22 et plus)
Avant Go 1.22, une boucle for avait une seule variable de boucle pour toute la boucle. Les closures créées à l'intérieur capturaient toutes cette variable unique et voyaient sa valeur finale. C'était le bug de closure le plus fréquent en Go.
Depuis Go 1.22, chaque itération reçoit sa propre copie, donc le code ci-dessous affiche 0 1 2 :
Avec Go 1.21 et avant, la même boucle écrite for i := 0; i < 3; i++ (le range sur un entier est aussi une nouveauté de 1.22) affichait 3 3 3. La nouvelle règle s'applique par module, selon la ligne go du go.mod, donc un ancien module garde l'ancien comportement tant que sa ligne go n'est pas passée à 1.22 ou plus. Vous verrez encore i := i ou v := v dans des boucles de code ancien : c'était la correction manuelle, elle est désormais redondante.
La règle par itération ne couvre que les variables déclarées par l'instruction for elle-même. Une variable déclarée avant la boucle et modifiée dans le corps reste une seule variable partagée :
var last string
for _, s := range items {
last = s
handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}
Closures et goroutines
Une goroutine lancée avec un littéral de fonction est aussi une closure. Les mêmes règles de partage s'appliquent, plus la règle habituelle de la concurrence : si plusieurs goroutines écrivent une variable capturée, il vous faut un mutex ou un channel.
Ce code affiche toujours 5050. Chaque goroutine lit son propre i (sémantique de boucle de Go 1.22) et écrit le total partagé sous verrou. Retirez le mutex et le résultat devient imprévisible ; go run -race le signale comme data race. Voir les goroutines pour WaitGroup et le détecteur de races.
Où l'on trouve des closures dans le vrai code
- Tri et recherche :
slices.SortFunc,slices.IndexFunc,sort.Sliceprennent tous un littéral de fonction. - Nettoyage différé :
defer func() { ... }()exécute un bloc à la sortie de la fonction et peut lire les variables de la fonction. Voir defer. - Middleware HTTP : une fonction qui prend un handler et renvoie un nouveau littéral
http.HandlerFuncqui l'enveloppe. - Configuration : les options fonctionnelles (
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) sont des closures sur l'argument.
Un exemple de middleware qui capture à la fois un paramètre et un compteur :
Erreurs courantes
- S'attendre à un instantané. Une closure lit la valeur actuelle de la variable au moment où elle s'exécute, pas celle du moment où elle a été créée. Passez la valeur en paramètre si vous voulez un instantané :
go func(v int) { ... }(x). - Littéraux récursifs. Un littéral de fonction ne peut pas se référer à lui-même via la variable à laquelle il est affecté dans le même
:=. Déclarez d'abord la variable :var walk func(n int); walk = func(n int) { ... walk(n-1) }. - Écritures non synchronisées depuis des goroutines. Capturer n'est pas synchroniser. Protégez les écritures partagées.
Questions fréquentes
Qu'est-ce qu'une fonction anonyme en Go ?
Un littéral de fonction sans nom : func(x int) int { return x * 2 }. Vous pouvez l'affecter à une variable, le passer en argument, le renvoyer, ou l'appeler immédiatement en ajoutant () après l'accolade fermante. Go n'a pas de syntaxe lambda à part ; le littéral de fonction est la lambda.
Qu'est-ce qu'une closure en Go ?
Un littéral de fonction qui fait référence à des variables déclarées à l'extérieur. La fonction garde ces variables en vie et les partage avec la portée englobante, donc les modifications faites dans la closure sont visibles à l'extérieur et inversement. Les variables sont capturées par référence, pas copiées.
Les closures Go capturent-elles les variables par valeur ou par référence ?
Par référence. La closure et le code environnant utilisent la même variable. Si vous avez besoin d'un instantané, copiez la valeur dans une nouvelle variable avant de créer la closure, ou passez-la en argument.
Le bug de la variable de boucle dans les closures est-il corrigé en Go ?
Oui, depuis Go 1.22. Chaque itération d'une boucle for déclare désormais une nouvelle variable de boucle, donc les closures et les goroutines créées dans la boucle voient la valeur de leur itération. Cela s'applique aux modules dont le go.mod indique go 1.22 ou plus. Le code plus ancien contient souvent i := i dans la boucle comme contournement ; c'est sans danger mais plus nécessaire.