iteracion-range-go-slices-mapas

Range in Go: Iterating slices, maps, and strings efficiently

  • 5 min

range is the idiomatic way to iterate over collections in Go, whether they are slices, arrays, maps, strings, or channels.

We already have slices and maps full of data. Now the question arises: how do we iterate through all of that cleanly?

If you come from C, you might be tempted to always use a for i=0; i<len; i++. It works, yes, but in Go we have a much more powerful and expressive tool: the range clause.

Let’s use range with the main iterable types and understand what value we receive in each iteration.

Basic syntax with slices

When we apply range to a Slice (or an Array), in each iteration of the loop it returns two values:

  1. The Index (the position, starting at 0).
  2. A COPY of the Value at that position.
package main

import "fmt"

func main() {
    nombres := []string{"Ana", "Beto", "Carla"}

    for i, nombre := range nombres {
        fmt.Printf("At index %d is %s\n", i, nombre)
    }
}
Copied!

It’s clean, readable, and safe (you won’t go out of bounds).

The blank identifier _

Go is very strict: if you declare a variable, you have to use it. What happens if we only want to loop through the values and don’t care about the index? If we write for i, valor := ... and we don’t use i, the compiler will give us an error.

That’s why the underscore _ exists, known as the blank identifier. It discards the value we don’t need.

Only the value (ignore index)

for _, nombre := range nombres {
    fmt.Println(nombre)
}
Copied!

Only the index (ignore value)

If we only want the indices, we can omit the second variable. It’s not necessary to put _ at the end.

for i := range nombres {
    // Here we only use the index.
    // Useful if we want to modify the original array (we'll see why below).
    fmt.Println(i)
}
Copied!

The iteration value is a copy

The important detail is that the second variable returned by range is a COPY of the original value.

Look at this mental diagram:

If the slice contains integers []int, copying a number is trivial. If it contains large structures, however, the iteration variable receives a complete copy of the value (although the compiler can optimize some cases).

Cost of copies

Iterating by value over large structures is slow.

Modifying the copy doesn’t change the slice

If you modify the value variable (v), you are NOT modifying the original slice, you’re only changing the local copy.

numeros := []int{10, 20, 30}

for _, v := range numeros {
    v = v * 2 // We modify the COPY 'v'
}

fmt.Println(numeros) // [10 20 30] -> IT HASN'T CHANGED!
Copied!

The solution

If you need to modify the original slice or avoid heavy copies, iterate over the index and access the slice directly.

for i := range numeros {
    numeros[i] = numeros[i] * 2 // Direct memory access
}

fmt.Println(numeros) // [20 40 60] -> Now it works!
Copied!

Iterating over maps

Using range with maps is just as simple, but instead of index/value, we get Key/Value.

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

for pais, ciudad := range capitales {
    fmt.Printf("The capital of %s is %s\n", pais, ciudad)
}
Copied!

Remember: the iteration order of a map is not guaranteed. Don’t assume they will come out in the order you inserted them, nor alphabetically.

If you only want the keys:

for pais := range capitales {
    fmt.Println("Available country:", pais)
}
Copied!

range over strings

A string is a sequence of bytes. When iterating over it with range, Go decodes its UTF-8 code points and returns rune values. If it encounters an invalid UTF-8 sequence, it returns utf8.RuneError.

Notice this example with the letter “ñ” (which takes 2 bytes in UTF-8):

palabra := "Aña"

for i, letra := range palabra {
    fmt.Printf("Index: %d, Letter: %c, Type: %T\n", i, letra, letra)
}
Copied!

Output:

Index: 0, Letter: A, Type: int32 (rune)
Index: 1, Letter: ñ, Type: int32 (rune)
Index: 3, Letter: a, Type: int32 (rune)   <-- Jumped from 1 to 3!
Copied!
  • The index i is the position of the starting byte.
  • The value letra is a rune (a Unicode code point).
  • Notice it jumped from index 1 to 3 because ‘ñ’ occupied bytes 1 and 2.

With an indexed for (for i := 0; i < len(s); i++) we would iterate over the bytes of the encoding, not complete characters.

Returned values by type

TypeVariables k, v := rangeBehavior
Slice / Arrayindex, value_copyLinear (0 to N). Be careful with large copies.
Mapkey, value_copyOrder not guaranteed.
Stringbyte_index, runeDecodes UTF-8 automatically. Variable index jumps.
Channel (Channel)value(We’ll see it in concurrency) Waits for data until the channel is closed.