range es la forma de for que recorre una colección. Te da uno o dos valores por iteración, y cuáles son depende del tipo que recorres:
| Operando | Primer valor | Segundo valor | Notas |
|---|---|---|---|
slice []T, array [n]T | índice int | elemento T, una copia | |
puntero a array *[n]T | índice | elemento | |
| string | índice en bytes int | rune | decodifica UTF-8 |
map map[K]V | clave K | valor V | orden aleatorio |
channel chan T | elemento T | ninguno | hasta que se cierra el channel |
entero n | 0 a n menos 1 | ninguno | Go 1.22 |
func(yield func() bool) | ninguno | ninguno | Go 1.23 |
func(yield func(V) bool) | V | ninguno | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 1.23 |
Elegir qué variables recibes
for i, v := range s {} // both
for i := range s {} // index (or key) only
for _, v := range s {} // value only
for range s {} // neither: just repeat len(s) times
Declarar una variable que no usas es un error de compilación, así que quita o deja en blanco las que no necesites. for range 3 {} es la forma más corta de repetir algo tres veces.
El valor es una copia
En cada iteración, el elemento se copia en la variable del valor. Cambiar la variable no cambia la colección:
Copiar también cuesta tiempo cuando los elementos son structs grandes. Para un slice de structs grandes, for i := range s con s[i] evita la copia.
Los strings producen runes
Recorrer un string decodifica UTF-8. El primer valor es el desplazamiento en bytes donde empieza cada carácter, así que puede saltarse números:
Para recorrer bytes en lugar de caracteres, usa un bucle con índice, for i := 0; i < len(s); i++, o recorre []byte(s). Consulta runes y bytes para los detalles de UTF-8.
Maps: orden aleatorio
El orden de iteración de un map no está especificado, y el runtime lo aleatoriza para que el código no pueda llegar a depender de un orden. Para una salida determinista, ordena las claves:
Borrar entradas mientras recorres es seguro, y una entrada borrada a la que aún no se ha llegado no se produce. Añadir entradas está permitido pero es impredecible: una clave nueva puede aparecer o no más adelante en el mismo bucle. fmt.Println de un map imprime las claves ordenadas, y por eso la última línea es estable.
Channels: hasta que se cierran
Recorrer un channel recibe valores hasta que el channel se cierra y se vacía. Si nadie lo cierra, el bucle se bloquea para siempre (y si todas las goroutines están bloqueadas, el runtime se detiene con fatal error: all goroutines are asleep - deadlock!):
El que cierra el channel es el emisor, nunca el receptor. Consulta channels para los channels con buffer y select.
Enteros (Go 1.22)
range n cuenta desde 0 hasta n, sin incluirlo. El tipo de la variable del bucle es el tipo de n:
for i := range 3 {} // i is int: 0, 1, 2
for i := range uint8(3) {} // i is uint8
for range 0 {} // runs zero times
Un n negativo también se ejecuta cero veces. n se evalúa una vez, antes de que empiece el bucle.
Qué evalúa range, y cuándo
La expresión que va detrás de range se evalúa una vez, antes de la primera iteración. Con un slice, range usa entonces esa cabecera de slice, así que su longitud queda fijada:
El 99 añadido nunca se visita porque la longitud del bucle se fijó en 3. Pero nums[2] = 30 sí se ve, porque en ese momento nums todavía comparte su array subyacente con el slice que se recorre. Intercambia las dos líneas y el 30 también desaparece: el append supera la capacidad de 3, reserva un array nuevo y la escritura cae allí. Recorrer un valor array copia primero el array entero, así que el cambio en arr[2] no se ve; recorre &arr o arr[:] para evitar la copia.
range sobre funciones (Go 1.23)
Desde Go 1.23, range también acepta funciones iteradoras. Un iterador recibe un callback yield, lo llama una vez por valor y se detiene cuando yield devuelve false (lo que ocurre cuando el cuerpo del bucle ejecuta break o return):
iter.Seq[V] e iter.Seq2[K, V] son los nombres estándar de las dos formas de iterador. La librería estándar los devuelve desde slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All, y en Go 1.24 desde strings.SplitSeq, strings.Lines y bytes.SplitSeq. El iterador tiene que respetar el valor de retorno de yield: llamar otra vez a yield después de que devolviera false provoca un panic.
Las variables de bucle son por iteración (Go 1.22)
Desde Go 1.22, las variables que declara for ... range son nuevas en cada iteración. Capturar v en una closure o una goroutine captura el valor de esa iteración, no una variable compartida que acaba conteniendo el último elemento. Antes de Go 1.22 necesitabas v := v dentro del bucle; esa línea ahora sobra. La regla la fija la versión go de go.mod, así que un módulo antiguo mantiene el comportamiento antiguo.
Errores comunes
- Tomar la dirección de la variable del valor. Antes de Go 1.22,
ptrs = append(ptrs, &v)guardaba siempre la misma dirección. Desde 1.22 guarda direcciones distintas, pero cada una apunta a una copia, no al elemento del slice. Usa&items[i]para apuntar dentro del slice. - Esperar un orden en los maps. Una salida que parece ordenada en un test pequeño no se mantendrá ordenada.
- Recorrer un channel que nadie cierra. El bucle, y la goroutine que lo ejecuta, esperan para siempre.
- Recorrer un array grande por valor. Primero se copia el array entero. Recorre un slice de él.
Preguntas frecuentes
¿Qué devuelve range en Go?
Depende del operando. Para un slice o un array: índice y elemento. Para un string: índice en bytes y rune. Para un map: clave y valor. Para un channel: cada valor recibido. Para un entero n (Go 1.22): de 0 a n menos 1. Para una función iteradora (Go 1.23): lo que produzca la función. Puedes omitir la segunda variable o descartar la primera con _.
¿Por qué modificar el valor de range no cambia mi slice?
La variable del valor es una copia del elemento. for _, v := range items { v.Price = 0 } solo cambia la copia. Usa el índice: for i := range items { items[i].Price = 0 }, o recorre un slice de punteros.
¿Se puede usar range sobre un entero en Go?
Sí, desde Go 1.22: for i := range 5 { ... } se ejecuta con i de 0 a 4. for range 5 { ... } repite cinco veces sin variable. El go.mod del módulo tiene que indicar go 1.22 o posterior.
¿Es seguro borrar de un map mientras lo recorres?
Sí. Borrar una entrada a la que aún no se ha llegado hace que no se produzca, y borrar la entrada actual no da problemas. Añadir entradas durante el bucle también está permitido, pero una entrada nueva puede visitarse o no, así que no dependas de ello.