Voor Go 1.22 wees de taalspecificatie lusvariabelen één keer per lusstatement toe in plaats van per iteratie. Deze enkele geheugenslocatie werd hergebruikt voor elke iteratie, waarbij alleen de waarde sequentieel veranderde. Wanneer een closure deze variabele via referentie vastlegde—wat gebruikelijk is in goroutines die binnen de lus worden gestart—deelden alle closures hetzelfde geheugenadres. Dit resulteerde erin dat elke closure de laatste waarde observeerde die aan dat adres werd toegewezen zodra de lus was voltooid.
Go 1.22 introduceerde per-iteratie scoping, wat betekent dat elke iteratie een nieuwe variabele met een ander geheugenadres instelt. Dit zorgt ervoor dat closures de specifieke waarde voor die iteratie vastleggen in plaats van een gedeelde wijzigbare locatie. Deze wijziging heeft een van de meest voorkomende valkuilen in de concurrency geëlimineerd en tegelijkertijd de achterwaartse compatibiliteit voor code behouden die niet afhankelijk was van de adresidentiteit van lusvariabelen.
Een dataverwerkingsdienst moest sensorlezingen naar werkgoroutines verspreiden voor parallelle validatie voorafgaand aan opslag.
Het team implementeerde aanvankelijk de fan-out met behulp van idiomatische closure syntax:
readings := []SensorReading{{ID: 1}, {ID: 2}, {ID: 3}} for _, r := range readings { go func() { validate(r.ID) // Kritieke bug: Alle goroutines valideren ID 3 }() }
Bij de implementatie toonde de log aan dat elke werknemer hetzelfde laatste record verwerkte, terwijl eerdere records volledig werden genegeerd, wat leidde tot gegevensverlies.
Oplossing 1: Variabele schaduwing. Deze benadering introduceert een nieuwe variabele binnen de lusbody om de iteratievariabele te schaduwen, waardoor een aparte stacktoewijzing voor elke iteratie wordt afgedwongen. Voordelen: Het lost onmiddellijk het capture-probleem op zonder wijzigingen in functiehandtekeningen te vereisen. Nadelen: Het steunt op een subtiele lexicale truc die syntactisch overbodig lijkt voor reviewers en biedt geen compilerbescherming als het per ongeluk wordt verwijderd tijdens refactoring.
Oplossing 2: Parameter doorgeven. Deze methode geeft expliciet de waarde door als argument aan de closure, waardoor de evaluatie plaatsvindt tijdens elke iteratie in plaats van op het moment van aanroep. Voordelen: Het is ondubbelzinnig, draagbaar over alle Go versies, en maakt gegevensafhankelijkheden expliciet en zelfdocumenterend. Nadelen: Het vereist herstructurering van de closure om parameters te accepteren, wat een minimale maar niet-nul syntactische overhead met zich meebrengt.
Oplossing 3: Infrastructuur upgrade. Het hele wagenpark migreren naar Go 1.22+ om gebruik te maken van de nieuwe per-iteratie variabele semantiek. Voordelen: Het elimineert de onderliggende oorzaak op taaloniveau, waardoor duidelijker idiomatische code mogelijk is. Nadelen: Het vereist gecoördineerde infrastructuurwijzigingen en biedt geen verlichting voor legacy codebases die op oudere toolchains moeten blijven.
Het team koos voor Oplossing 2 voor onmiddellijke implementatie. Deze beslissing zorgde ervoor dat de code correct functioneerde op alle compiler versies en niet afhankelijk was van subtiele schaduwingstricks die per ongeluk konden worden verwijderd.
Na implementatie ontving elke goroutine zijn eigen unieke sensor-ID, verwerkte de pijplijn alle records correct en bleef het systeem stabiel tijdens de daaropvolgende upgrade naar Go 1.22.
Waarom staat het nemen van het adres van een for-range iteratievariabele in Go 1.22+ nog steeds geen directe wijziging van de originele slice-elementen toe?
Zelfs met per-iteratie variabelen houdt de iteratievariabele een kopie van het slice-element vast, niet het element zelf. Het nemen van zijn adres levert een pointer op naar deze ephemere kopie in plaats van naar de invoer in de onderliggende array. Aangezien de variabele van elke iteratie een aparte locatie is maar een kopie van de waarde bevat, beïnvloedt het wijzigen van *(&v) alleen de tijdelijke kopie, die wordt weggegooid wanneer de iteratie eindigt. Om de bron slice te wijzigen, moet je indexsyntax gebruiken: for i := range slice { slice[i].Field = NewValue }.
Introduceert de per-iteratie scoping wijziging in Go 1.22 prestatie overhead of aanvullende heap-toewijzingen in vergelijking met het model van variabele hergebruik vóór 1.22?
Nee. De Go compiler optimaliseert per-iteratie variabelen zodat ze op de stack of in registers verblijven wanneer closures niet naar de heap ontsnappen. De semantische wijziging heeft invloed op lexicale scoping en pointer identiteit, niet op de toewijzingsstrategie of runtime prestaties van de lus zelf. Lussen zonder closures vertonen identieke prestatiekenmerken voor en na de wijziging.
Hoe beïnvloedde het gedrag van variabele hergebruik in pre-1.22 Go traditionele drie-clausule for lussen vergeleken met for-range lussen?
Het gedrag was identiek voor alle for lusvarianten. Zowel for i := 0; i < n; i++ als for _, v := range m hergebruikten hetzelfde geheugenadres voor hun iteratievariabelen over alle iteraties. Kandidaten gaan vaak ten onrechte ervan uit dat de stale closure bug uniek was voor range lussen, maar closures die de index i in een drie-clausule lus vastlegden, hadden hetzelfde probleem, waarbij de laatste waarde van i werd afgedrukt in plaats van de verwachte iteratiewaarde. Go 1.22 heeft dit uniform opgelost voor alle loop types.