The range Clause
How to use Go's range keyword to iterate over arrays, slices, strings, maps, channels, integers, and custom iterator functions.
The range clause is Go's primary way to walk through the elements of a collection. Instead of manually tracking an index, checking a length, and incrementing a counter, you let range hand you each element — or key‑value pair — one at a time. It works on arrays, slices, strings, maps, channels, and, starting with Go 1.22, integers. With Go 1.23 it also works over custom iterator functions, which lets you write for ... range loops over your own data structures.
How range Works Mechanically
When you write for index, element := range collection, the compiler rewrites the loop so that each iteration sets index and element to the next pair of values from the collection. The precise values you get depend on the type of the collection.
Think of range as a helper that already knows how far to go. If you were reading pages in a book, a plain for loop is you counting “page 1, page 2, …” until you hit the last page. range is a bookmark that turns the page for you and hands you the content without you needing to track the page number. You still get the page number if you want it, but you can ignore it.
Type determines the iteration values:
The number of variables on the left side of := or = must match the type being ranged, or you can use _ to discard values you don’t need. Using := creates new loop-local variables; using = assigns to existing ones.
Ranging Over Arrays and Slices
The most common use of range is with slices and arrays. It returns two values per iteration: the index and a copy of the element at that index.
numbers := []int{10, 20, 30}
for i, v := range numbers {
fmt.Printf("Index: %d, Value: %d\n", i, v)
}
This prints:
Index: 0, Value: 10
Index: 1, Value: 20
Index: 2, Value: 30
If you only need the element, discard the index with _:
for _, v := range numbers {
fmt.Println(v)
}
When you omit both the index and the value — for range numbers { ... } — the loop body executes once per element but does not expose the data. This is useful when you just need the side effects.
Nil pointer to a slice causes a panic:
A nil slice is safe to range over (the loop body runs zero times). A nil pointer to a slice, however, is not. Attempting for _, v := range *nilSlicePtr will panic with a nil pointer dereference. Always ensure a pointer to a slice is non‑nil before dereferencing it.
Range Copies the Entire Array
When you use range on an array value (not a slice), Go copies the whole array before iterating. For a large array that can be expensive. Prefer slicing the array or taking its address if you want to avoid the copy:
arr := [3]int{1, 2, 3}
for i, v := range arr { // arr is copied each time the loop starts
fmt.Println(i, v)
}
// Avoid the copy by ranging over a slice of the array:
for i, v := range arr[:] {
fmt.Println(i, v)
}
If your output matches:
When you run a range loop over a known slice and see the elements printed in order, the iteration is working correctly. No extra setup is required — range handles the bounds for you.
Ranging Over Strings — Rune Decoding
Ranging over a string does not give you individual bytes. It decodes the UTF‑8 encoded string into Unicode code points (runes) and returns the byte index of each rune along with the rune itself.
s := "café"
for index, runeValue := range s {
fmt.Printf("Byte index %d: %c\n", index, runeValue)
}
Output:
Byte index 0: c
Byte index 1: a
Byte index 2: f
Byte index 3: é
The é occupies two bytes, so the next index after it would be 5, not 4. This is the crucial difference between indexing a string with s[i] (which gives a byte) and ranging over it (which gives whole runes).
String indexing vs. ranging:
s[3] on the string "café" returns the first byte of the é character, not the whole rune. Always use range if you need to process characters rather than raw bytes.
If you need only the runes and not the byte indices, write:
for _, r := range s {
fmt.Printf("%c", r)
}
Ranging Over Maps
When you range over a map, you get the key and the value for each entry. The iteration order is deliberately not specified — it is randomized from the perspective of the programmer, and you must not rely on any particular sequence.
scores := map[string]int{"Alice": 95, "Bob": 87}
for name, score := range scores {
fmt.Printf("%s: %d\n", name, score)
}
If you only need the keys, omit the second variable:
for name := range scores {
fmt.Println(name)
}
Ranging over a nil map is safe; the loop simply executes zero times.
Map mutation during iteration:
Adding or deleting entries from a map while iterating over it has undefined behavior. The loop may produce inconsistent results, skip entries, or visit entries twice. If you need to delete specific entries, collect the keys first and delete after the loop.
Ranging Over Channels
A range loop over a channel reads values from it repeatedly until the channel is closed. Once closed, the loop exits. This is the idiomatic way to consume a stream of values.
ch := make(chan int, 3)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println(v)
}
// Output: 1 2
If the channel is never closed, the loop will block forever, waiting for more values or a close. You must ensure another goroutine eventually calls close(ch) when no more values will be sent.
Ranging Over Integers (Go 1.22+)
Before Go 1.22, writing a loop that runs n times required a classic three‑clause for statement. Go 1.22 added a shorthand: for i := range n iterates from 0 to n-1.
for i := range 5 {
fmt.Println(i)
}
// Output: 0 1 2 3 4
You can ignore the index just as with other collections:
for range n {
// body runs n times
}
This is especially handy in benchmarks where the loop body itself needs to execute b.N times:
for range b.N {
// code under test
}
Zero‑based and exclusive upper bound:
range n always starts at 0 and stops before n. It does not support negative numbers or custom start values. For those cases, a classic for loop is still the right tool.
Ranging Over Function Iterators (Go 1.23+)
Go 1.23 introduced the ability to range over a function that follows a specific signature. This lets custom containers expose a for ... range loop without exposing their internal structure. The function must accept a yield argument that it calls for each element.
The standard library provides two generic iterator types in the iter package:
iter.Seq[V]— a sequence of single values.iter.Seq2[K, V]— a sequence of key‑value pairs.
An iterator function returns one of these types. Inside, it calls the yield function for each element, and checking the boolean return from yield allows early termination (when the caller uses break or return).
A minimal example: a List container with an All iterator.
package main
import (
"fmt"
"iter"
)
type List[T any] struct {
items []T
}
func (l *List[T]) All() iter.Seq[T] {
return func(yield func(T) bool) {
for _, item := range l.items {
if !yield(item) {
return
}
}
}
}
func main() {
lst := &List[int]{items: []int{10, 20, 30}}
for v := range lst.All() {
fmt.Println(v)
}
// Output: 10 20 30
}
The transformation is done by the compiler. The loop body becomes the yield function. If the loop body executes break, the corresponding yield call returns false, and the iterator function should stop producing values. A continue becomes a return true from yield, instructing the iterator to keep going.
For key‑value pairs, use iter.Seq2 and provide two parameters to yield:
func (m *MyMap[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m.internal {
if !yield(k, v) {
return
}
}
}
}
Then you can range as:
for k, v := range myMap.All() {
// use k and v
}
Forgetting to check yield’s return value:
If your iterator ignores the boolean returned by yield and keeps looping, the break inside the calling for‑range will not stop iteration. This can cause wasted work or, worse, an infinite loop. Always write if !yield(...) { return }.
Early termination works correctly:
When you test your iterator by calling break in the loop body and see that the program exits the loop without hanging, your implementation correctly respects the early‑stop protocol.
Common Mistakes When Using range
Expecting Sorted Order from Maps
Map iteration order is undefined. If you need sorted keys, extract them into a slice and sort it separately, then iterate over the sorted slice while looking up values.
Modifying the Collection During Iteration
Adding or deleting entries in a map while ranging over it leads to unpredictable behavior. For slices, changing the length (e.g., via append that reallocates) may invalidate the iteration. If you must remove elements, iterate over a copy or collect indices first.
Taking the Address of the Loop Variable
The loop variable is reused across iterations. If you capture &v inside the loop body, you’ll always get the same address, which will hold the last iteration’s value after the loop finishes.
var out []*int
for _, v := range []int{1, 2, 3} {
out = append(out, &v)
}
for _, p := range out {
fmt.Println(*p) // prints 3 3 3, not 1 2 3
}
Go 1.22 changed the semantics so that the loop variable is fresh each iteration, but only when you are using Go 1.22 or later with the module’s go directive set to 1.22. For older versions, or when working with code that hasn’t adopted the new semantics, the reuse hazard remains.
Confusing Bytes and Runes in Strings
As described earlier, for i, c := range s gives runes, while s[i] gives a byte. Ranging over a string is the only built‑in way to correctly walk UTF‑8 characters.
Summary
The range clause is the idiomatic way to traverse Go’s built‑in collections and, with recent language versions, integers and custom iterator functions. It reduces the chance of off‑by‑one errors, automatically handles length detection, and decodes strings into runes. When you need more control — a non‑zero start, a step other than one, or complex early‑exit logic — the classic three‑clause for loop remains the right choice. For everything else, range keeps the loop concise and the intent clear.