Avant Go 1.22, la spécification du langage attribuait les variables de boucle une seule fois par instruction de boucle plutôt que par itération. Cet emplacement mémoire unique était réutilisé pour chaque itération, seule sa valeur changeant de manière séquentielle. Lorsqu'une fermeture capturait cette variable par référence — ce qui est courant dans les goroutines lancées à l'intérieur de la boucle — toutes les fermetures partageaient la même adresse mémoire. Par conséquent, chaque fermeture observait la valeur finale attribuée à cette adresse une fois la boucle terminée.
Go 1.22 a introduit une portée par itération, ce qui signifie que chaque itération instancie une nouvelle variable avec une adresse mémoire distincte. Cela garantit que les fermetures capturent la valeur spécifique de cette itération plutôt qu'un emplacement mutable partagé. Ce changement a éliminé l'un des pièges de concurrence les plus courants tout en maintenant la compatibilité avec le code qui ne dépendait pas de l'identité d'adresse des variables de boucle.
Un service de traitement de données devait dispatcher des lectures de capteurs vers des goroutines de travail pour une validation parallèle avant stockage.
L'équipe avait initialement mis en œuvre le dispatching en utilisant la syntaxe des fermetures idiomatiques :
readings := []SensorReading{{ID: 1}, {ID: 2}, {ID: 3}} for _, r := range readings { go func() { validate(r.ID) // Bug critique : Toutes les goroutines valident l'ID 3 }() }
Lors du déploiement, les journaux ont révélé que chaque travailleur ne traitait que le même dernier enregistrement, tandis que les enregistrements précédents étaient complètement ignorés, causant une perte de données.
Solution 1 : Ombre de variable. Cette approche introduit une nouvelle variable à l'intérieur du corps de la boucle pour obscurcir la variable d'itération, forçant une allocation de pile distincte pour chaque itération. Avantages : Cela résout immédiatement le problème de capture sans nécessiter de modifications des signatures de fonction. Inconvénients : Cela repose sur un subtil truc lexical qui paraît syntaxiquement redondant pour les examinateurs et ne fournit aucune protection du compilateur en cas de suppression accidentelle lors du refactoring.
Solution 2 : Passage de paramètres. Cette méthode passe explicitement la valeur en tant qu'argument à la fermeture, garantissant que l'évaluation se produit à chaque itération plutôt qu'au moment de l'appel. Avantages : Il n'y a aucune ambiguïté, il est portable à travers toutes les versions de Go, et rend les dépendances de données explicites et auto-documentées. Inconvénients : Cela nécessite de restructurer la fermeture pour accepter des paramètres, ce qui ajoute un surcoût syntaxique minime mais non nul.
Solution 3 : Mise à niveau de l'infrastructure. Migrer l'ensemble de la flotte vers Go 1.22+ pour tirer parti des nouvelles sémantiques de variable par itération. Avantages : Cela élimine la cause racine au niveau du langage, permettant un code idiomatique plus propre. Inconvénients : Cela nécessite des changements d'infrastructure coordonnés et n'offre aucun soulagement pour les bases de code héritées qui doivent rester sur d'anciens outils.
L'équipe a choisi Solution 2 pour un déploiement immédiat. Cette décision a garanti que le code se comportait correctement à travers toutes les versions du compilateur et ne reposait pas sur des astuces subtiles d'ombre qui pourraient être accidentellement supprimées.
Après l'implémentation, chaque goroutine a reçu son ID de capteur distinct, le pipeline a traité tous les enregistrements correctement, et le système est resté stable lors de la mise à niveau ultérieure vers Go 1.22.
Pourquoi le fait de prendre l'adresse d'une variable d'itération for-range dans Go 1.22+ ne permet toujours pas la modification directe des éléments de l'original ?
Même avec des variables par itération, la variable d'itération contient une copie de l'élément du slice, pas l'élément lui-même. Prendre son adresse donne un pointeur vers cette copie éphémère plutôt que l'entrée dans le tableau sous-jacent. Étant donné que la variable de chaque itération est un emplacement distinct mais contient une copie de la valeur, modifier *(&v) n'affecte que la copie temporaire, qui est rejetée à la fin de l'itération. Pour modifier le slice source, vous devez utiliser la syntaxe d'index : for i := range slice { slice[i].Field = NewValue }.
Le changement de portée par itération dans Go 1.22 introduit-il des frais de performance ou des allocations de tas supplémentaires par rapport au modèle de réutilisation des variables avant 1.22 ?
Non. Le compilateur Go optimise les variables par itération pour résider sur la pile ou dans des registres lorsque les fermetures ne s'échappent pas dans le tas. Le changement sémantique affecte la portée lexicale et l'identité des pointeurs, pas la stratégie d'allocation ou les performances d'exécution de la boucle elle-même. Les boucles sans fermetures affichent des caractéristiques de performance identiques avant et après le changement.
Comment le comportement de réutilisation des variables dans le Go avant 1.22 a-t-il affecté les boucles traditionnelles à trois clauses par rapport aux boucles for-range ?
Le comportement était identique à travers toutes les variantes de boucles for. Tant for i := 0; i < n; i++ que for _, v := range m réutilisaient la même adresse mémoire pour leurs variables d'itération à travers toutes les itérations. Les candidats supposent souvent que le bug de fermeture obsolète était unique aux boucles range, mais les fermetures capturant l'indice i dans une boucle à trois clauses souffraient du même problème, imprimant la valeur finale de i plutôt que la valeur d'itération attendue. Go 1.22 a résolu cela uniformément pour tous les types de boucles.