ProgrammationDéveloppeur Go senior

Parlez-nous de la façon dont Go implémente des fermetures (fonctions littérales/closures) et quelles sont les limitations et particularités de leur utilisation : où elles sont stockées, comment les variables sont capturées, comment le comportement des variables capturées diffère selon différents scénarios ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse

En Go, les fonctions anonymes (fonctions littérales) sont capables de créer des fermetures, c'est-à-dire d'accéder aux variables de l'environnement même après la fin de celui-ci. De telles fermetures allouent de la mémoire dans le tas si nécessaire pour un fonctionnement correct (détecté via l'analyse d'échappement).

Exemple :

func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } a := adder() printf("%d ", a(5)) // 5 printf("%d ", a(10)) // 15

Particularités :

  • La fermeture capture les variables de l'environnement par référence (et non leurs valeurs à la création).
  • Si une variable est modifiée en dehors de la fermeture, la fermeture verra la nouvelle valeur.
  • Si la fermeture est retournée d'une fonction, les variables capturées vivront jusqu'à la fin de la durée de vie de la fermeture.
  • Si la fermeture n'est pas utilisée, l'analyse d'échappement peut permettre aux variables de ne pas aller dans le tas.

Question piège

Que va imprimer ce code ?

func main() { fs := []func(){} for i := 0; i < 3; i++ { fs = append(fs, func() { fmt.Println(i) }) } for _, f := range fs { f() } }

Beaucoup diront qu'il imprimera 0, 1, 2, mais le résultat sera :

3
3
3

Toutes les fermetures pointent vers la même variable i ; après la fin de la boucle, sa valeur est 3.

Correct : capturer une copie de la variable dans le corps de la boucle :

for i := 0; i < 3; i++ { v := i // nouvelle variable fs = append(fs, func() { fmt.Println(v) }) }

Exemples d'erreurs réelles dues à l'ignorance des subtilités du sujet


Histoire

Dans un projet de routage dynamique, nous utilisions une boucle pour créer de nombreux gestionnaires via des fermetures, chacun devait capturer son propre chemin. En conséquence, tous les gestionnaires imprimaient le dernier chemin — nous n'avions pas créé de variable séparée dans chaque fermeture. L'erreur n'a été détectée qu'au moment de l'intégration avec l'API HTTP.


Histoire

Lors des tests d'accès parallèle via des goroutines, la fermeture capturait la référence à l'index, et non la copie. Cela créait des effets « aléatoires » : les données étaient écrites non dans leur slot de tableau, mais dans le dernier.


Histoire

Dans une fonction de collecte de statistiques, la fermeture modifiait une variable partagée de l'environnement externe, bien que l'auteur s'attendait à un compteur indépendant pour chaque tâche. Le problème a été remarqué par la somme de reconstruction inadéquate, qui était toujours globale, pas locale, malgré la logique locale.