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:
- The Index (the position, starting at 0).
- 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)
}
}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)
}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)
}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!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!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)
}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)
}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)
}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!- The index
iis the position of the starting byte. - The value
letrais arune(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
| Type | Variables k, v := range | Behavior |
|---|---|---|
| Slice / Array | index, value_copy | Linear (0 to N). Be careful with large copies. |
| Map | key, value_copy | Order not guaranteed. |
| String | byte_index, rune | Decodes UTF-8 automatically. Variable index jumps. |
| Channel (Channel) | value | (We’ll see it in concurrency) Waits for data until the channel is closed. |