Go has exactly one loop construct: for. It covers all looping scenarios — the classic C-style three-clause loop, a while-style condition-only loop, an infinite loop, and the range form for iterating over collections. No while, no do-while, no foreach — just for in different configurations.
range is the idiomatic way to iterate in Go. It works over slices, arrays, maps, strings (yielding runes, not bytes), and channels.
The Three Loop Forms
// 1. Three-clause: init; condition; post
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 2. Condition-only (while-style)
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n) // 128
// 3. Infinite loop — must break manually
for {
if done() {
break
}
doWork()
}
Go’s for requires braces — no single-line loops. The init and post statements in the three-clause form can be any simple statement: multiple assignment, function call, blank statement.
range Over Slices and Arrays
range returns an index-value pair. Use _ to discard either:
nums := []int{10, 20, 30, 40, 50}
// Both index and value
for i, v := range nums {
fmt.Printf("nums[%d] = %d\n", i, v)
}
// Value only
for _, v := range nums {
fmt.Println(v)
}
// Index only (range itself, without the value)
for i := range nums {
nums[i] *= 2 // modify in place — must use index, not range value
}
A critical distinction: the v in for _, v := range nums is a copy. To modify elements, use the index form nums[i].
range Over Maps
Map iteration order is randomized by design — don’t rely on it:
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Printf("%s: %d\n", k, v) // order varies each run
}
// Keys only
for k := range m {
delete(m, k) // safe to delete during range
}
Adding keys to a map during range iteration may or may not be visited in the same loop — the spec doesn’t guarantee it.
range Over Strings Gives Runes
Ranging a string iterates over Unicode code points (runes), not bytes. The index is the byte position where the rune starts:
s := "Hello, 世界"
for i, r := range s {
fmt.Printf("s[%d] = %c (%U)\n", i, r, r)
}
// s[0] = H (U+0048)
// s[7] = 世 (U+4E16) ← byte index 7, not character index 7
// s[10] = 界 (U+754C) ← byte index 10 (世 is 3 bytes)
If you need byte iteration, index the string directly (s[i] gives byte). If you need character-indexed access, convert to []rune first.
range Over Channels
range on a channel reads values until the channel is closed:
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // range exits when channel closes
}()
for v := range ch {
fmt.Println(v)
}
// 0 1 2 3 4
If the channel is never closed and the goroutine stops sending, range blocks forever — a goroutine leak. Always ensure the sender closes the channel when done.
break and continue
// break exits the innermost loop
for i := 0; i < 10; i++ {
if i == 5 { break }
fmt.Println(i)
}
// continue skips to the next iteration
for i := 0; i < 10; i++ {
if i%2 == 0 { continue }
fmt.Println(i) // prints odd numbers only
}
Labeled break and continue for Nested Loops
Without labels, break and continue only affect the innermost loop. Labels target a specific outer loop:
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // exits the i loop entirely
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// (0,0) (0,1) (0,2) (1,0)
search:
for i, row := range matrix {
for j, val := range row {
if val == target {
fmt.Printf("found at (%d,%d)\n", i, j)
break search // stop searching — value found
}
}
}
Labels are rare but useful for search patterns where finding the value should stop the entire search, not just the inner loop.
Common Patterns
Sum / fold:
sum := 0
for _, v := range nums {
sum += v
}
Find first matching element:
var found *User
for i := range users {
if users[i].Role == "admin" {
found = &users[i]
break
}
}
Build a filtered slice:
var active []User
for _, u := range users {
if u.Active {
active = append(active, u)
}
}
Drain a channel with timeout:
for {
select {
case v, ok := <-ch:
if !ok { return } // channel closed
process(v)
case <-ctx.Done():
return
}
}
Go 1.22: Range Over Integers
Go 1.22 added range over integers — a cleaner way to loop N times:
// Go 1.22+
for i := range 5 {
fmt.Println(i) // 0 1 2 3 4
}
// Equivalent to
for i := 0; i < 5; i++ { ... }
Summary
- Go has one loop (
for) in three forms: three-clause, condition-only, infinite rangeis idiomatic for iterating slices/arrays, maps, strings (by rune), and channels- The
vinfor _, v := range sliceis a copy — modify elements via indexslice[i] - Map iteration order is random — never depend on it
breakandcontinueaffect the innermost loop; labels target specific outer loops- Close channels that are being ranged —
rangeblocks until the channel closes
Comments