Skip to main content

Go Loops: for, range, and Loop Patterns

Published: December 17, 2025 Updated: August 29, 2026 Larry Qu 5 min read

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
  • range is idiomatic for iterating slices/arrays, maps, strings (by rune), and channels
  • The v in for _, v := range slice is a copy — modify elements via index slice[i]
  • Map iteration order is random — never depend on it
  • break and continue affect the innermost loop; labels target specific outer loops
  • Close channels that are being ranged — range blocks until the channel closes

Resources

Comments

👍 Was this article helpful?