for Loop Variants & range Semantics
Go expresses every loop with for, but the shape you choose changes what you copy, what you can mutate, and when the loop stops.
This page maps each for variant to the collections and channels you will see in production code.
Summary
The three-part for header covers counted loops.
The condition-only form acts like while.
for range walks slices, arrays, maps, channels, and strings with type-specific index/value meaning.
Channels block until a value arrives or the channel closes.
Maps iterate in randomized order.
Strings yield byte offsets and runes, not bytes directly.
Recipe
Quick-reference recipe card - copy-paste ready.
package main
import "fmt"
func main() {
// C-style
for i := 0; i < 3; i++ {
fmt.Println("count", i)
}
// while-style
n := 3
for n > 0 {
n--
}
// range slice
for i, v := range []int{10, 20} {
fmt.Println(i, v)
}
}When to reach for this:
- Use three-part
forwhen you need an index counter with a clear end condition. - Use condition-only
forfor event loops, retries, and channel pumps (for { select { ... } }). - Use
for rangefor idiomatic iteration over collections and open channels. - Use
for rangeover1..nalternatives via slices orfor i := 0; i < n; i++- Go has no inclusive range operator.
Working Example
package main
import (
"fmt"
"time"
)
func main() {
// Slice: mutate by index
nums := []int{1, 2, 3}
for i := range nums {
nums[i] *= 2
}
fmt.Println("slice", nums)
// Map: keys and values (order varies each run)
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println("map", k, v)
}
// Channel: receive until closed
ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)
for v := range ch {
fmt.Println("chan", v)
}
// String: byte index and rune
for i, r := range "Go🙂" {
fmt.Printf("string i=%d r=%U\n", i, r)
}
// Infinite loop with timeout break
deadline := time.Now().Add(50 * time.Millisecond)
for {
if time.Now().After(deadline) {
break
}
}
fmt.Println("done")
}What this demonstrates:
- Slice mutation requires index access, not the range value copy.
- Map iteration order is not stable - never depend on it.
for rangeon a channel ends when the channel is closed and drained.- String
rangedecodes UTF-8 runes; index jumps by byte width for multibyte characters. - Condition-only infinite loops pair with
break,return, orselect.
Deep Dive
How It Works
forcreates a block scope; the init variable in a three-part loop is scoped to theforstatement.rangeevaluates the collection once; for arrays/slices it copies the header, not the backing array elements.- Channel receive in
for rangeblocks until a value is ready; zero value plusokpattern is implicit in range form. - Map iterators pick random start buckets - order differs across processes and runs by design.
- String range converts bytes to runes;
len(s)is byte length, which differs from rune count.
range by Type
| Type | Index (1st value) | Value (2nd value) | Notes |
|---|---|---|---|
| Slice / array | int index | element copy | mutate with s[i], not v |
| Map | key copy | value copy | order randomized |
| Channel | (none) | received element | ends after close + drain |
| String | byte offset | rune (int32) | offset advances by UTF-8 width |
for Loop Shapes
| Shape | Syntax | Typical use |
|---|---|---|
| Three-part | for i := 0; i < n; i++ | counted loops |
| Condition only | for cond | while-style, read until EOF |
| Infinite | for { } | servers, select loops |
| Range | for k, v := range x | collections, channels, strings |
Go Notes
// Omit value or index with blank identifier
for _, v := range items { _ = v }
for i := range items { _ = i }
// range over array copies elements for value form on large arrays - use index
var big [10000]byte
for i := range big {
big[i] = 1
}Gotchas
- Mutating slice elements via range value -
for _, v := range s { v = 1 }updates a copy. Fix: use index:for i := range s { s[i] = 1 }. - Depending on map iteration order - Tests assuming key order flake. Fix: sort keys first or compare multisets.
- Ranging an open channel forever - Without close,
for range chblocks forever. Fix: close from sender side or useselectwithctx.Done(). - Confusing string length with rune count -
len("é")is 2 bytes in UTF-8. Fix: userangeorutf8.RuneCountInString. - Large array range copies - Ranging over array value copies each element. Fix: range indices on arrays or use slices.
- Off-by-one with three-part loops - Using
<=when<was intended. Fix: preferfor i := 0; i < len(s); i++idioms.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
for range | Idiomatic iteration | You need reverse iteration without extra index math |
Index for i := 0; ... | Need previous/next element by index | A simple range loop suffices |
slices.Backward / manual index | Reverse walk | Forward range is enough |
for { select } | Multiplex channels and timers | Single collection walk |
| Stdlib iterators (Go 1.23+) | Custom sequences with yield | Simple slice/map walks |
FAQs
Does Go have a while loop?
No - for condition { } is the while form.
The keyword is always for.
Can I range over a nil slice or map?
Yes - both are no-ops with zero iterations.
A nil map is still unreadable for assignment without make.
Why does map order change between runs?
Go randomizes map iteration start to prevent reliance on order.
Sort keys when you need deterministic output.
When does for range on a channel stop?
After the channel is closed and all buffered values are received.
An open channel blocks forever on the next receive.
Should I use the range value on large structs?
The value is a copy - expensive for big structs.
Use index or pointer slice []*T when profiling shows copy cost.
How do I loop n times without a collection?
Use for i := 0; i < n; i++.
There is no for i := range n in older Go; Go 1.22+ allows for i := range n when n is an integer.
What does for i := range n do in Go 1.22+?
It iterates i from 0 through n-1 when n is a non-negative integer type.
Useful for counted loops without manual init/post.
Can I delete map keys while ranging?
Yes - deleting the current key is safe.
Deleting other keys during iteration follows map semantics; keep logic simple.
How do I break out of a select loop?
Use break on a label or return from the function.
break alone exits the select, not the surrounding for.
Is ranging a string slow?
It decodes UTF-8, which costs more than raw byte scans.
For ASCII-only data, index bytes directly with bounds checks.
Related
- Control Flow Basics - intro snippets for
forandrange - break, continue & Labeled Control Flow - exiting nested loops
- range Variable Reuse Pitfalls - closure capture in loops
- Go Control Flow: Expression-Oriented Design - conceptual overview
- Arrays, Slices & Maps at a Glance - collection types iterated here
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).