range en Go: iterar slices, maps, strings y canales de forma idiomática

El operador range de Go es la forma idiomática de recorrer colecciones. Funciona con slices, arrays, maps, strings y canales, y en cada caso devuelve información diferente. Aprender sus peculiaridades —especialmente con strings Unicode— evita errores que son fáciles de introducir y difíciles de depurar.

range sobre slices y arrays

package main

import "fmt"

func main() {
    frutas := []string{"manzana", "pera", "naranja"}

    // índice y valor
    for i, fruta := range frutas {
        fmt.Printf("[%d] %sn", i, fruta)
    }

    // solo índice
    for i := range frutas {
        frutas[i] = strings.ToUpper(frutas[i])
    }

    // solo valor (ignorar índice con _)
    for _, f := range frutas {
        fmt.Println(f)
    }
}

range sobre maps

Con maps, range devuelve clave y valor. El orden es aleatorio en cada ejecución:

capitales := map[string]string{
    "España":   "Madrid",
    "Francia":  "París",
    "Alemania": "Berlín",
}

for pais, capital := range capitales {
    fmt.Printf("%s ? %sn", pais, capital)
}

// iterar solo las claves
for pais := range capitales {
    fmt.Println(pais)
}

range sobre strings: runas, no bytes

Este es el comportamiento que más sorprende. Con una cadena, range itera sobre runas Unicode (puntos de código), no sobre bytes. El índice es la posición del byte donde comienza la runa, no un índice secuencial:

s := "Go: ñoño"

// range sobre string: runas Unicode
for i, r := range s {
    fmt.Printf("i=%d runa=%c (%d)n", i, r, r)
}
// i=0 runa=G (71)
// i=1 runa=o (111)
// i=2 runa=: (58)
// i=3 runa=  (32)
// i=4 runa=ñ (241)  ? ñ ocupa 2 bytes en UTF-8; el siguiente i será 6
// i=6 runa=o (111)
// i=7 runa=ñ (241)
// i=9 runa=o (111)

Si necesitas el índice de bytes y el byte en sí, convierte a []byte antes:

for i, b := range []byte(s) {
    fmt.Printf("i=%d byte=%dn", i, b)
}

El error con structs en range

La variable de iteración en range es una copia. Modificarla no afecta al slice original:

type Punto struct{ X, Y int }

puntos := []Punto{{1, 2}, {3, 4}, {5, 6}}

// MAL: p es una copia, el slice no cambia
for _, p := range puntos {
    p.X *= 10
}
fmt.Println(puntos) // [{1 2} {3 4} {5 6}] sin cambios

// BIEN: modifica por índice
for i := range puntos {
    puntos[i].X *= 10
}
fmt.Println(puntos) // [{10 2} {30 4} {50 6}]

range sobre canales

Con un canal, range lee valores hasta que el canal se cierra. Es la forma idiomática de consumir todos los resultados de un pipeline:

func generador(n int) <-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; i < n; i++ {
            ch <- i
        }
        close(ch) // imprescindible para que range termine
    }()
    return ch
}

func main() {
    for v := range generador(5) {
        fmt.Println(v) // 0 1 2 3 4
    }
}

Ignorar valores con _

Si no necesitas el índice o la clave, usa _ para ignorarlo. Go exige que todas las variables declaradas se usen; el identificador en blanco es la válvula de escape:

suma := 0
for _, v := range []int{1, 2, 3, 4, 5} {
    suma += v
}
fmt.Println(suma) // 15

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP