En Go, la construcción del ciclo for puede incluir un bloque de inicialización (init), una verificación de condición y una expresión postfija. Históricamente, este mecanismo se creó para facilitar la escritura de código y la familiaridad con lenguajes similares a C. Sin embargo, en Go, el ámbito de la variable del ciclo (i) tiene especificidades que afectan en gran medida el comportamiento dentro de funciones anidadas, closures y goroutines.
Problema — Al iniciar goroutines o closures en cada iteración del ciclo, a menudo se observa un comportamiento inesperado: la variable i no se copia, sino que se "captura" por referencia, es decir, el closure accede a la variable compartida del ciclo, que al finalizar el ciclo toma el último valor. Esto lleva a que todas las goroutines/closures obtengan el mismo resultado, aunque la lógica podría haber supuesto algo diferente.
Solución — Si es necesario pasar el valor de la variable de cada iteración, utilice la copia explícita de la variable (a través de una variable adicional) o pase su valor como argumento al closure.
Ejemplo de código:
for i := 0; i < 3; i++ { go func(j int) { fmt.Println(j) }(i) // ¡Correcto! Valor copiado } for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Error: todas las goroutines imprimirán 3 }
Características clave:
¿Cambia el ámbito de la variable for al usar break o continue?
No. El ámbito de la variable declarada en for siempre está limitado al bloque de ese ciclo. Break o continue solo interrumpen la iteración actual, pero no "sacan" la variable afuera.
¿Se puede capturar una variable declarada en la parte init de for dentro de un método fuera del ciclo?
No. La variable es visible solo dentro del propio for y de todos los bloques anidados en él, pero no fuera de él después de que el ciclo se completa.
¿Qué sucederá si la variable se captura en una expresión defer dentro de for?
La misma situación: la función defer "verá" no el valor en el momento de la creación, sino el valor actual de la variable en el momento de la ejecución del defer (por lo general, el último valor del ciclo).
for i := 0; i < 3; i++ { defer fmt.Println(i) // todos los defer imprimirán 3 }
En un servidor web Go, un desarrollador lanzaba varias goroutines para manejar diferentes puertos, usando el índice del puerto como variable de ciclo y capturándola directamente en la expresión lambda. Todas las goroutines accedían a un solo puerto: el último en el array.
Pros:
Contras:
En el equipo se estableció la regla de siempre copiar el valor de la variable de ciclo en una nueva variable, que luego captura el closure/goroutine.
Pros:
Contras: