Back to Notes

Go Loops

Overview

Go has exactly one looping construct: for. It replaces C's for, while, and do-while. No parentheses around the loop header, braces always required. This keeps the language minimal without sacrificing expressiveness.


C-Style for Loop

The classic loop with init, condition, and post statement. Init runs once before the loop, condition is checked before each iteration, post runs after each iteration.

for i := 0; i < 5; i++ {
    fmt.Println(i)
}
// 0 1 2 3 4

// Step by 2
for j := 0; j < 10; j += 2 {
    fmt.Println(j)
}
// 0 2 4 6 8

// Count down
for i := 5; i > 0; i-- {
    fmt.Print(i, " ")
}
// 5 4 3 2 1

While-Style Loop

Drop the init and post — just keep the condition. Equivalent to while in other languages.

n := 1
for n < 1000 {
    n *= 2
}
fmt.Println(n)  // 1024

// Read lines until EOF
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

Infinite Loop

Omit the condition entirely. Use break to exit, return to exit the whole function.

for {
    input := readInput()
    if input == "quit" {
        break
    }
    process(input)
}

range — Iterating Collections

range produces an index+value pair for each element. It works on slices, arrays, maps, strings, and channels. Use _ to discard either the index or the value.

Slice / Array

nums := []int{10, 20, 30, 40}

for i, v := range nums {
    fmt.Printf("index %d = %d\n", i, v)
}

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

Map

Order is not guaranteed — randomised on each run by design.

scores := map[string]int{"Alice": 95, "Bob": 87, "Carol": 92}

for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

for name := range scores {   // keys only
    fmt.Println(name)
}

String

range on a string yields Unicode runes (int32), not bytes. Multi-byte characters are handled correctly.

for i, ch := range "Go🚀" {
    fmt.Printf("index %d: %c (%d)\n", i, ch, ch)
}
// index 0: G (71)
// index 1: o (111)
// index 2: 🚀 (128640)   — one rune, but 4 bytes

Channel

range over a channel reads until the channel is closed.

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

for v := range ch {
    fmt.Println(v)  // 0, 1, 2
}

continue and break

  • continue — skip the remaining code in the current iteration, jump to the next one
  • break — exit the loop immediately, regardless of condition
// Print only odd numbers
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue      // skip even
    }
    fmt.Println(i)    // 1 3 5 7 9
}

// Find first negative number
nums := []int{3, 7, -1, 4, -2}
for _, n := range nums {
    if n < 0 {
        fmt.Println("first negative:", n)
        break   // -1
    }
}

Labeled Loops — Targeting Outer Loops

A label lets break or continue target an outer loop directly. This avoids awkward boolean flag variables in nested loops.

// Without labels — needs a flag
found := false
for i := 0; i < 5 && !found; i++ {
    for j := 0; j < 5; j++ {
        if matrix[i][j] == target {
            found = true
            break
        }
    }
}

// With labels — cleaner
outer:
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if matrix[i][j] == target {
                fmt.Printf("found at (%d, %d)\n", i, j)
                break outer   // exits both loops
            }
        }
    }

// continue outer — skip to next i, not next j
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if j == 1 {
                continue outer
            }
            fmt.Printf("i=%d j=%d\n", i, j)
        }
    }
// i=0 j=0 / i=1 j=0 / i=2 j=0

Common Loop Patterns

Sum a slice

nums := []int{1, 2, 3, 4, 5}
sum := 0
for _, n := range nums {
    sum += n
}
// sum = 15

Reverse a slice in place

s := []int{1, 2, 3, 4, 5}
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    s[i], s[j] = s[j], s[i]
}
// s = [5 4 3 2 1]

Filter into new slice

nums := []int{1, 2, 3, 4, 5, 6}
var evens []int
for _, n := range nums {
    if n%2 == 0 {
        evens = append(evens, n)
    }
}
// evens = [2 4 6]

Find max

nums := []int{3, 1, 4, 1, 5, 9, 2, 6}
max := nums[0]
for _, n := range nums[1:] {
    if n > max {
        max = n
    }
}
// max = 9

Collect map keys, sorted

import "sort"

m := map[string]int{"b": 2, "a": 1, "c": 3}
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])
}

Gotchas

GotchaDetail
Loop variable capture (Go < 1.22)Closures inside loops capture the loop variable by reference — all closures see the final value. Use v := v to create a local copy. Go 1.22+ fixes this.
Map iteration orderRandomised per run — do not rely on order
range on nil sliceSafe — iterates zero times
range string gives runes not bytesMulti-byte Unicode chars span multiple byte indices
Modifying slice while rangingChanging elements by index is safe; append may create a new backing array