ProgramaciónDesarrollador Go

¿Cómo funciona la declaración de ciclo for-init en Go y por qué las características del ámbito de la variable de ciclo pueden llevar a errores difíciles de detectar al usarse en goroutines y closures?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

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:

  • En el ciclo for, la variable del ciclo se declara implícitamente en el ámbito del bloque for
  • La captura de la variable del ciclo en un closure/goroutine llevará a la "división" de la variable entre todas las instancias del closure
  • Se evita realizando una copia de la variable en una nueva variable en cada iteración

Preguntas trampa.

¿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 }

Errores comunes y anti-patterns

  • Captura de la variable de ciclo sin copiarla a una nueva variable
  • Paso de la variable de ciclo a una función anónima sin pasarla explícitamente (efecto de late binding)
  • Uso de defer dentro del ciclo sin considerar el ámbito de las variables

Ejemplo de la vida real

Caso negativo

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:

  • Implementación de ciclo simple y "explícita"

Contras:

  • Lógica de funcionamiento incorrecta
  • Bug difícil de resolver

Caso positivo

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:

  • Sin efectos secundarios inesperados
  • Transparencia del código

Contras:

  • Se perdieron "microoptimizaciones" (una variable más en la pila, pero no es significativo)