ProgrammationDéveloppeur Go

Comment fonctionne la déclaration de cycle for-init postfix en Go, et pourquoi les particularités de la portée de la variable de boucle peuvent entraîner des bogues difficiles à détecter lors de l'utilisation dans des goroutines et des closures ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En Go, la construction de boucle for peut inclure un bloc d'initialisation (init), une vérification de condition et une expression postfixe. Historiquement, ce mécanisme a été créé pour faciliter l'écriture de code et par habitude des langages semblables à C. Cependant, en Go, la portée de la variable de boucle (i) a des particularités qui influencent fortement le comportement à l'intérieur des fonctions imbriquées, des closures et des goroutines.

Problème — Lors de l'exécution de goroutines ou de closures à chaque itération de la boucle, un comportement inattendu survient souvent : la variable i n'est pas copiée mais "capturée" par référence, c'est-à-dire que la closure accède à la variable de boucle partagée, qui, après la fin de la boucle, prend la dernière valeur. Cela conduit à des résultats identiques dans toutes les goroutines/closures, bien que la logique ait pu présager autre chose.

Solution — Si vous devez transmettre la valeur de la variable pour chaque itération, utilisez une copie explicite de la variable (via une variable supplémentaire) ou passez-la comme argument dans la closure.

Exemple de code :

for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // Correct ! Valeur copiée } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Erreur : toutes les goroutines imprimeront 3 }

Caractéristiques clés :

  • Dans la boucle for, la variable de boucle est déclarée implicitement dans la portée du bloc for.
  • La capture de la variable de boucle dans une closure/goroutine entraînera le "partage" de la variable entre toutes les instances de la closure.
  • Résolu par la copie de la variable dans une nouvelle variable à chaque itération.

Questions pièges.

La portée de la variable for change-t-elle lors de l'utilisation de break ou de continue ?

Non. La portée de la variable déclarée dans for est toujours limitée au bloc de cette boucle. Break ou continue ne font que interrompre l'itération en cours, mais ne "lancent" pas la variable à l'extérieur.

Peut-on capturer la variable déclarée dans la partie init de for, à l'intérieur d'une méthode en dehors de la boucle ?

Non. La variable n'est visible qu'à l'intérieur du for lui-même et de tous les blocs imbriqués, mais pas à l'extérieur après la fin de la boucle.

Que se passera-t-il si la capture de la variable se produit dans une expression defer à l'intérieur du for ?

Même situation : la fonction defer "verra" non pas la valeur au moment de la création, mais la valeur actuelle de la variable au moment de l'exécution du defer (généralement, la dernière valeur de la boucle).

for i := 0; i < 3; i++ { defer fmt.Println(i) // tous les defer imprimeront 3 }

Erreurs courantes et anti-patterns

  • Capture de la variable de boucle sans copie dans une nouvelle variable.
  • Transmission de la variable de boucle dans une fonction anonyme sans la passer explicitement (effet de liaison tardive).
  • Utilisation de defer à l'intérieur d'une boucle sans tenir compte de la portée des variables.

Exemple de la vie réelle

Cas négatif

Dans un serveur web Go, le développeur a lancé plusieurs goroutines pour traiter différents ports, en utilisant l'indice du port comme variable de boucle et en capturant directement celle-ci dans une expression lambda. Toutes les goroutines accédaient à un seul port — le dernier du tableau.

Avantages :

  • Implémentation cyclique simple et "explicite".

Inconvénients :

  • Logique de fonctionnement incorrecte.
  • Bogues difficiles à déchiffrer.

Cas positif

L'équipe a introduit une règle — toujours copier la valeur de la variable de boucle dans une nouvelle variable, qui sera ensuite capturée par la closure/goroutine.

Avantages :

  • Pas d'effets secondaires inattendus.
  • Transparence du code.

Inconvénients :

  • Perte de "micro-optimisations" (une autre variable dans la pile, mais peu significatif).