In Go, la struttura del ciclo for può includere un blocco di inizializzazione (init), una condizione di controllo e un'espressione postfix. Questo meccanismo è stato creato storicamente per facilitare la scrittura di codice e per abituarsi ai linguaggi simili al C. Tuttavia, in Go la visibilità della variabile di ciclo (i) ha delle peculiarità che influiscono molto sul comportamento all'interno di funzioni annidate, chiusure e goroutine.
Problema — all'avvio di goroutine o chiusure ad ogni iterazione del ciclo spesso si verifica un comportamento inaspettato: la variabile i non viene copiata, ma "catturata" per riferimento, cioè la chiusura fa riferimento a una variabile di ciclo condivisa che dopo la fine del ciclo assume l'ultimo valore. Questo porta a risultati identici in tutte le goroutine/chiusure, anche se la logica potrebbe suggerire diversamente.
Soluzione — se è necessario trasmettere il valore della variabile in ogni iterazione, utilizzare una copia esplicita della variabile (tramite una variabile aggiuntiva) oppure passarla come argomento alla chiusura.
Esempio di codice:
for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // Corretto! Valore copiato } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Errore: tutte le goroutine stamperanno 3 }
Caratteristiche chiave:
Cambia la visibilità della variabile for quando si utilizza break o continue?
No. La visibilità della variabile dichiarata nel for è sempre limitata al blocco di quel ciclo. Break o continue interrompono solo l'iterazione corrente, ma non "propagano" la variabile all'esterno.
Si può catturare una variabile dichiarata nella parte init del for, all'interno di un metodo al di fuori del ciclo?
No. La variabile è visibile solo all'interno del for stesso e di tutti i blocchi annidati in esso, ma non al di fuori dopo il completamento del ciclo.
Cosa succede se la cattura della variabile avviene in un'espressione defer all'interno del for?
La stessa situazione: la funzione defer "vedrà" non il valore al momento della creazione, ma il valore attuale della variabile al momento dell'esecuzione del defer (di solito l'ultimo valore del ciclo).
for i := 0; i < 3; i++ { defer fmt.Println(i) // tutte le defer stamperanno 3 }
In un server web Go, uno sviluppatore ha avviato diverse goroutine per gestire porte diverse, utilizzando l'indice della porta come variabile di ciclo e catturandola direttamente nell'espressione lambda. Tutte le goroutine facevano riferimento a una sola porta — l'ultima nell'array.
Pro:
Contro:
Nel team è stata introdotta la regola — copiare sempre il valore della variabile di ciclo in una nuova variabile, che poi cattura la chiusura/goroutine.
Pro:
Contro: