Dominio de la iteración con for-range en Go

for-range es la instrucción diseñada para recorrer colecciones y estructuras de datos de forma idiomática. Más que un simple bucle, es una construcción que abstrae la lógica de iteración según el tipo de dato con el que trabajas. Cuando usas for-range, el runtime de Go decide la forma más eficiente de navegar los elementos, ya sea saltando por índices en un slice, recorriendo pares clave-valor en un map, o decodificando caracteres Unicode en un string.

Para entender su funcionamiento, hay que entender cómo entrega los datos. Al iterar sobre un slice o un array, puedes obtener dos valores: el índice y una copia del elemento. Es fundamental comprender que ese segundo valor es una copia de los datos en la posición actual; cualquier modificación sobre esa variable no afectará al slice original. En cambio, si iteras sobre un map, Go te entrega la clave y el valor, pero el orden de iteración es aleatorio para garantizar que el diseño del sistema no dependa de un orden interno que podría cambiar.

Cuando trabajas con strings, el comportamiento cambia: range no itera byte a byte, sino que decodifica runes (puntos de código Unicode). Esto significa que el primer valor que recibes es el byte offset (la posición en bytes), no necesariamente el índice de carácter, lo cual es crítico cuando manejas caracteres especiales o emojis. Con los channels, el bucle se bloquea en cada iteración esperando un valor y termina automáticamente cuando el canal se cierra con close. Finalmente, desde Go 1.22 [disponible desde Go 1.22], puedes usar for range n para repetir un bloque de código un número exacto de veces sin necesidad de declarar un contador manual.

Si intentas modificar un elemento de un slice usando la variable de valor obtenida en el range, estarás trabajando sobre una copia y tus cambios se perderán silenciosamente. Si esperas un orden constante en un map, tu lógica de negocio fallará de forma intermitente.

package main

import (
	"fmt"
)

type User struct {
	Name   string
	Active bool
}

func main() {
	// 1. Iteración sobre un slice de structs
	// Usamos solo el índice para poder modificar el elemento original
	users := []User{
		{Name: "Alice", Active: false},
		{Name: "Bob", Active: false},
	}
	for i := range users {
		users[i].Active = true // Modificamos directamente en la posición del slice
	}

	// 2. Iteración sobre un map (orden aleatorio)
	permissions := map[string]string{
		"admin": "read-write",
		"guest": "read-only",
	}
	fmt.Println("--- Permisos (orden variable) ---")
	for role, access := range permissions {
		fmt.Printf("Rol: %s, Acceso: %s\n", role, access)
	}

	// 3. Iteración sobre un string (runes y byte offsets)
	// El emoji 🚀 ocupa más de un byte.
	msg := "Go 🚀"
	fmt.Println("\n--- String (runes) ---")
	for offset, runeVal := range msg {
		fmt.Printf("Byte offset: %d, Carácter: %c\n", offset, runeVal)
	}

	// 4. Iteración sobre un channel
	fmt.Println("\n--- Canal de tareas ---")
	tasks := make(chan string, 2)
	go func() {
		tasks <- "Limpiar caché"
		tasks <- "Sincronizar DB"
		close(tasks) // Crucial para que el range termine
	}()

	for task := range tasks {
		fmt.Println("Ejecutando:", task)
	}

	// 5. Iteración sobre un entero (Go 1.22+)
	// Repite el bloque exactamente 3 veces.
	fmt.Println("\n--- Reintentos ---")
	for range 3 {
		fmt.Println("Intentando conexión...")
	}
}

Análisis del código

En el primer bloque, al iterar sobre users, nos limitamos a obtener el índice i mediante for i := range users. Si hubiéramos usado for i, u := range users, la variable u sería una copia de la estructura User. Al hacer u.Active = true, solo estaríamos activando la copia, dejando el slice original intacto.

En la sección del map permissions, notarás que role y access extraen la clave y el valor. Debido a la implementación interna de los mapas en Go, si ejecutas este programa varias veces, el orden de salida de “admin” y “guest” puede variar, lo que te obliga a no confiar en la secuencia durante el procesamiento.

En la parte del string msg, observa el offset. El carácter ‘G’ está en el offset 0, ‘o’ en el 1, el espacio en el 2. Sin embargo, el emoji 🚀 no ocupa un solo byte. El siguiente offset que imprime el bucle tras el emoji será el 6, saltándose los bytes intermedios porque range es inteligente y sabe que debe entregar el siguiente rune completo, no el siguiente byte.

Para el channel tasks, el bucle for task := range tasks es la forma más limpia de consumir un canal hasta que se cierra. Si olvidaras el close(tasks) en la goroutine, el bucle principal se quedaría bloqueado para siempre (deadlock) esperando más datos.

Por último, el bucle for range 3 utiliza la nueva sintaxis para simplificar contadores cuando el índice no tiene ninguna utilidad lógica en tu algoritmo, reduciendo el ruido visual.

El error frecuente

Un error clásico es intentar actualizar un slice de structs usando el segundo valor del range.

// ERROR: El cambio no se refleja en el slice
users := []User{{Name: "Alice", Active: false}}
for _, u := range users {
    u.Active = true // 'u' es una copia local de la iteración
}
fmt.Println(users[0].Active) // Imprimirá: false

Como u es una copia, el compilador simplemente actualiza una variable temporal que desaparece en la siguiente iteración. Para modificar el contenido real, debes acceder siempre mediante el índice: users[i].Active = true.

36

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio