Go by Example

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