En Go, el bucle for con range ha sido históricamente una fuente de errores sutiles cuando se combina con la concurrencia. El problema reside en cómo se gestionan las variables de iteración. Hasta la versión 1.21, la variable que declaras en un for i, v := range slice no era una nueva variable en cada paso del bucle, sino una única variable que se reutilizaba, actualizando su valor en cada iteración. Este cambio de semántica introducido en Go 1.22 [disponible desde Go 1.22] redefine esto: ahora, cada iteración del bucle crea una nueva instancia de la variable.
Este ajuste no es un cambio caprichoso; responde a un problema de diseño fundamental. En el modelo antiguo, si lanzabas una goroutine que hiciera referencia a v dentro del bucle, todas las goroutines terminaban capturando la misma dirección de memoria. Debido a que las goroutines no se ejecutan instantáneamente, para cuando la goroutine empezaba a leer v, el bucle probablemente ya había avanzado o terminado, haciendo que todas las goroutines vieran el último valor del slice. Es un bug clásico que ha costado horas de depuración en sistemas distribuidos.
La solución de Go 1.22 es más intuitiva: cada iteración tiene su propia copia de la variable, lo que permite que las closures capturen el valor exacto de ese paso específico sin necesidad de trucos manuales. Aunque esto es el comportamiento estándar ahora, es vital entenderlo para leer código heredado o para entender por qué antes era necesario hacer task := task (una técnica para crear una copia local en el scope de la iteración) para evitar desastres. En la práctica, este cambio es extremadamente seguro y casi nunca rompe código existente, a menos que estuvieras haciendo algo muy poco común como esperar que la dirección de memoria de la variable de loop fuera la misma en cada iteración.
Para probar esto antes de que fuera el estándar, Go 1.21 permitió activar este comportamiento mediante la variable de entorno GOEXPERIMENT=loopvar.
package main
import (
"fmt"
"sync"
)
// Job representa una tarea de procesamiento en nuestro sistema.
type Job struct {
ID int
Name string
}
func main() {
// Simulamos una lista de trabajos que deben procesarse de forma concurrente.
jobs := []Job{
{ID: 1, Name: "Sincronizar base de datos"},
{ID: 2, Name: "Limpiar caché de Redis"},
{ID: 3, Name: "Generar reporte de auditoría"},
}
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
// En versiones anteriores a Go 1.22, lanzar la goroutine así
// causaría que todas las goroutines imprimieran el último Job.
// Con Go 1.22, 'job' es una variable nueva para cada iteración.
go func() {
defer wg.Done()
// La variable 'job' se captura por valor para esta iteración específica.
process(job)
}()
}
// Esperamos a que todas las goroutines terminen para evitar que el programa
// finalice antes de que se impriman los resultados.
wg.Wait()
}
func process(j Job) {
// Simulamos un procesamiento breve.
fmt.Printf("Ejecutando tarea [ID: %d]: %s\n", j.ID, j.Name)
}
Análisis del comportamiento
En el código anterior, fíjate en la estructura del for _, job := range jobs. En la versión moderna de Go, cuando el compilador ve esa iteración, asigna un nuevo espacio de memoria para job en cada paso.
Cuando ejecutamos go func() { ... }(), la goroutine captura la variable job de la iteración actual. Gracias a que cada iteración tiene su propia instancia, el valor de job queda “atrapado” correctamente para esa goroutine, independientemente de lo rápido que el bucle principal continúe hacia la siguiente iteración. Si hubieras usado el modelo antiguo, el wg.Wait() se completaría, pero es muy probable que la salida del terminal mostrara tres veces la tarea “Generar reporte de auditoría” (el último elemento), porque todas las goroutines habrían estado mirando la misma dirección de memoria que fue actualizada por el bucle.
El error frecuente
Antes de Go 1.22, este era el patrón que causaba dolores de cabeza en producción:
// ❌ Código que fallaba en Go < 1.22
for _, v := range data {
go func() {
fmt.Println(v) // Todas las goroutines imprimirían el último valor de 'data'
}()
}
// ✅ Forma correcta (manual) en Go < 1.22
for _, v := range data {
v := v // Creamos una copia local para el scope de la iteración
go func() {
fmt.Println(v)
}()
}
// ✅ Forma correcta y nativa en Go 1.22+
for _, v := range data {
go func() {
fmt.Println(v) // Ya es seguro
}()
}
Comprender este cambio te permite distinguir entre un bug de lógica y el comportamiento histórico de la plataforma.
N° 37