Range over Built-in Types
The range keyword iterates over slices, maps, strings, channels, and integers - each with different iteration semantics worth knowing.
range is Go's universal iteration keyword. It adapts to the type it ranges over: slices give index and value, maps give key and value, strings give byte-index and rune, channels give values as they arrive, and since Go 1.22 integers give a count from 0.
Ranging over a slice yields the index and a copy of the element. Use _ to discard whichever you don't need.
package main
import "fmt"
func main() {
fruits := []string{"apple", "banana", "cherry"}
for i, v := range fruits {
fmt.Println(i, v)
}
// 0 apple
// 1 banana
// 2 cherry
// Index only
for i := range fruits {
fmt.Println(i)
}
// Value only
for _, v := range fruits {
fmt.Println(v)
}
}Ranging over a map yields key-value pairs. Map iteration order is deliberately randomised by the runtime on each run.
package main
import (
"fmt"
"sort"
)
func main() {
scores := map[string]int{"alice": 90, "bob": 85, "carol": 92}
// Order is non-deterministic
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
// Deterministic output - sort keys first
keys := make([]string, 0, len(scores))
for k := range scores {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, scores[k])
}
}Ranging over a string decodes UTF-8 runes, not bytes. The index is the byte offset of the rune in the string, not its position in a character array.
package main
import "fmt"
func main() {
s := "héllo"
// range gives (byte-index, rune)
for i, r := range s {
fmt.Printf("index=%d rune=%c\n", i, r)
}
// index=0 rune=h
// index=1 rune=é (é is 2 bytes in UTF-8)
// index=3 rune=l
// index=4 rune=l
// index=5 rune=o
fmt.Println(len(s)) // 6 - byte length
fmt.Println(len([]rune(s))) // 5 - rune (character) count
}Since Go 1.22, you can range over an integer n to iterate from 0 to n-1 - a concise replacement for for i := 0; i < n; i++.
package main
import "fmt"
func main() {
// Go 1.22+: range over integer
for i := range 5 {
fmt.Println(i)
}
// 0 1 2 3 4
}In production
Two surprises appear constantly in code review. First, map iteration order is non-deterministic - if you need deterministic output (logs, serialized responses, test assertions), sort the keys explicitly before ranging. Second, ranging over a string decodes UTF-8 runes, so the index jumps by byte width, not by 1. len(s) returns bytes; len([]rune(s)) returns rune count. Mixing these silently corrupts string operations on any non-ASCII input - a bug that only surfaces when users with non-Latin names or emoji-heavy data start using your product.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free