Polyglot
Languages Go loops
Go § loops

Loops

Go has one loop construct: for. The keyword serves four roles via different clause forms — three-clause C-style (for init; cond; post), condition-only (for cond), infinite (for), and range-based (for ... range). The unification eliminates the C-family while/do-while/for distinction. Like other Go control constructs, for requires braces and admits no parenthesised header. The combination — single keyword, mandatory braces, four clause forms, range over slices/maps/strings/channels — covers the iteration surface.

The four for forms

Three-clause form

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

The conventional C-style: an initialisation statement, a condition, and a post statement. Each part is optional but the semicolons are required if any are present.

Condition-only form

for n > 0 {
    n--
    fmt.Println(n)
}

The form is Go’s while — runs while the condition is true.

Infinite form

for {
    // ...
    if done {
        break
    }
}

The form is Go’s while (true) — runs forever until break, return, or panic.

Range form

v := []int{10, 20, 30}
for i, x := range v {
    fmt.Printf("[%d] = %d\n", i, x)
}

The form admits iterating over slices, arrays, maps, strings, and channels. Treated below.

break and continue

for i := 0; i < 10; i++ {
    if i == 5 {
        break                                    // exit the loop
    }
    if i % 2 == 0 {
        continue                                  // skip to next iteration
    }
    fmt.Println(i)                               // 1, 3
}

break exits the innermost enclosing for, switch, or select; continue proceeds to the next iteration.

Labelled break and continue

For nested loops, labels admit targeting a specific loop:

outer:
for i := 0; i < 10; i++ {
    for j := 0; j < 10; j++ {
        if i*j > 50 {
            break outer                           // breaks the outer loop
        }
    }
}

next:
for _, line := range lines {
    for _, word := range strings.Fields(line) {
        if word == "skip" {
            continue next                         // proceeds to next line
        }
        process(word)
    }
}

The mechanism is occasionally needed; conventionally, restructuring into a function with return is clearer.

The range form

range produces (index, value) pairs for slices/arrays, (key, value) pairs for maps, (index, rune) pairs for strings, and value-only for channels:

Range over a slice

v := []string{"a", "b", "c"}

for i, x := range v {                            // index and value
    fmt.Printf("[%d] = %s\n", i, x)
}

for _, x := range v {                            // value only
    fmt.Println(x)
}

for i := range v {                               // index only
    v[i] = strings.ToUpper(v[i])
}

The blank identifier _ admits discarding the index. The single-receiver form receives the index alone.

Range over an array

Same as slices:

arr := [3]int{1, 2, 3}
for i, x := range arr {
    fmt.Printf("[%d] = %d\n", i, x)
}

A subtle point: range over an array iterates over the value (a copy), not a reference. To modify, range over a pointer to the array or use a slice.

Range over a map

m := map[string]int{"a": 1, "b": 2, "c": 3}

for k, v := range m {                            // key and value
    fmt.Printf("%s = %d\n", k, v)
}

for k := range m {                               // key only
    fmt.Println(k)
}

The iteration order is randomised — Go deliberately produces different orders across runs to discourage code that depends on map order. For ordered iteration, sort the keys:

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Printf("%s = %d\n", k, m[k])
}

Range over a string

s := "héllo"

for i, r := range s {                            // byte index and rune
    fmt.Printf("byte %d: rune %c\n", i, r)
}

The mechanism produces the rune sequence (Unicode code points), with i being the byte index of each rune (since runes may be multi-byte in UTF-8).

For byte iteration, use the indexed form:

for i := 0; i < len(s); i++ {
    fmt.Printf("byte %d: %d\n", i, s[i])
}

Range over a channel

ch := make(chan int)
go func() {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}()

for v := range ch {                              // receive until channel is closed
    fmt.Println(v)
}

The form receives values until the channel is closed; if not closed, it blocks indefinitely.

Range over an integer (Go 1.22+)

for i := range 10 {
    fmt.Println(i)                               // 0, 1, ..., 9
}

The form is a recent addition; it admits “loop n times” without the explicit three-clause form.

Common patterns

Iterating with index

for i, x := range slice {
    fmt.Printf("[%d] = %v\n", i, x)
}

Iterating values only

for _, x := range slice {
    process(x)
}

Iterating in reverse

Slices admit reverse iteration via the indexed form:

for i := len(slice) - 1; i >= 0; i-- {
    fmt.Println(slice[i])
}

Modifying during iteration

// Index-based: safe
for i := range slice {
    slice[i] = transform(slice[i])
}

// Range-by-value: the value is a COPY; modifying x doesn't affect slice
for _, x := range slice {
    x = transform(x)                              // does NOT modify slice
}

For modifying, use the indexed form. For appending during iteration, range captures the slice’s length at the start; new appends are not visited:

v := []int{1, 2, 3}
for i, x := range v {
    v = append(v, x*2)                           // NOT iterated
    if i > 5 { break }
}
// v is [1, 2, 3, 2, 4, 6]; only the original 3 elements were iterated

Loop with state

state := initialState()
for !state.IsTerminal() {
    state = state.Step()
}

Polling

for {
    if condition() {
        break
    }
    time.Sleep(100 * time.Millisecond)
}

For condition-driven loops with no upfront iteration count, the infinite-for form is conventional.

Reading lines from stdin

scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    line := scanner.Text()
    process(line)
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

The pattern is the conventional Go form for reading input line by line.

Range over a generated sequence (Go 1.23+)

The range form admits iterator functions since Go 1.23:

import "iter"

func evens(max int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < max; i += 2 {
            if !yield(i) {
                return
            }
        }
    }
}

for n := range evens(10) {
    fmt.Println(n)                               // 0, 2, 4, 6, 8
}

The mechanism is recent; the conventional form for routine iteration remains slice/map/channel range.

Worker loop

func worker(jobs <-chan Job, results chan<- Result) {
    for job := range jobs {                       // receive until channel closes
        results <- process(job)
    }
}

The pattern is conventional in goroutine-based worker pools.

Retry loop

const maxAttempts = 5
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
    result, err := operation()
    if err == nil {
        return result, nil
    }
    lastErr = err
    time.Sleep(backoff(attempt))
}
return Result{}, fmt.Errorf("after %d attempts: %w", maxAttempts, lastErr)

Dual-channel select loop

for {
    select {
    case msg := <-messages:
        process(msg)
    case <-quit:
        return
    }
}

Treated in Concurrency.

Skip the rest with continue

for _, item := range items {
    if !item.Valid() {
        continue                                  // skip invalid
    }
    process(item)
}

Break on error

for _, line := range lines {
    if err := process(line); err != nil {
        log.Printf("processing failed: %v", err)
        break
    }
}

A note on the absence of while and do-while

Go does not have separate while and do-while. The two are expressed via for:

// while:
for cond {
    body
}

// do-while equivalent:
for {
    body
    if !cond { break }
}

The unification simplifies the language at the cost of slight verbosity in the do-while case.

A note on the conventional discipline

The contemporary Go loop advice:

  • Use for ... range for iteration; it’s the conventional form.
  • Use the three-clause form for indexed iteration with explicit step.
  • Use the condition-only form (for cond) for while-style loops.
  • Use the infinite form (for {}) for retry loops, polling, event loops.
  • Use break and continue freely.
  • Use labels rarely — restructure when nested-loop control is needed.
  • Use the blank identifier (_) to discard the index.
  • Sort keys for deterministic map iteration.

The combination — single for keyword, four clause forms, range over slices/maps/strings/channels, mandatory braces — is the substance of Go’s iteration surface. The discipline produces clear, explicit looping code.