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.
Search across all documentation pages
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.
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.
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:
for when you need an index counter with a clear end condition.for for event loops, retries, and channel pumps (for { select { ... } }).for range for idiomatic iteration over collections and open channels.for range over 1..n alternatives via slices or for i := 0; i < n; i++ - Go has no inclusive range operator.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:
for range on a channel ends when the channel is closed and drained.range decodes UTF-8 runes; index jumps by byte width for multibyte characters.break, return, or select.for creates a block scope; the init variable in a three-part loop is scoped to the for statement.range evaluates the collection once; for arrays/slices it copies the header, not the backing array elements.for range blocks until a value is ready; zero value plus ok pattern is implicit in range form.len(s) is byte length, which differs from rune count.| 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 |
| 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 |
// 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
}for _, v := range s { v = 1 } updates a copy. Fix: use index: for i := range s { s[i] = 1 }.for range ch blocks forever. Fix: close from sender side or use select with ctx.Done().len("é") is 2 bytes in UTF-8. Fix: use range or utf8.RuneCountInString.<= when < was intended. Fix: prefer for i := 0; i < len(s); i++ idioms.| 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 |
No - for condition { } is the while form.
The keyword is always for.
Yes - both are no-ops with zero iterations.
A nil map is still unreadable for assignment without make.
Go randomizes map iteration start to prevent reliance on order.
Sort keys when you need deterministic output.
After the channel is closed and all buffered values are received.
An open channel blocks forever on the next receive.
The value is a copy - expensive for big structs.
Use index or pointer slice []*T when profiling shows copy cost.
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.
It iterates i from 0 through n-1 when n is a non-negative integer type.
Useful for counted loops without manual init/post.
Yes - deleting the current key is safe.
Deleting other keys during iteration follows map semantics; keep logic simple.
Use break on a label or return from the function.
break alone exits the select, not the surrounding for.
It decodes UTF-8, which costs more than raw byte scans.
For ASCII-only data, index bytes directly with bounds checks.
for and rangeStack 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).
Reviewed by Chris St. John·Last updated Jul 18, 2026