Loops and Iteration

Updated

September 13, 2026

Loops and Iteration

Go has one loop: for. The boring default is a C-style for when you need an index you control, for i := range n when you need 0..n-1, and for _, v := range s when you need the elements. Do not range a map if the order of keys is the behaviour.

Mental model

Every loop is for. There is no while and no do. The condition-only form is for cond { }. The infinite form is for { }.

Since Go 1.22, for i := range n walks 0, 1, …, n-1 for an integer n. range over a slice gives index and value. range over a string gives byte index and rune. range over a map gives key and value in an order the runtime does not promise.

break leaves the inner-most loop or switch. A label names which loop to leave. continue skips to the next iteration of that loop.

Worked examples

Case 1: The three for shapes

Save as for_shapes.go. A counter, a condition, and an infinite loop that breaks.

// for_shapes.go
package main

import "fmt"

func main() {
    for i := 1; i <= 3; i++ {
        fmt.Println("table", i)
    }

    n := 3
    for n > 0 {
        fmt.Println("left", n)
        n--
    }

    k := 0
    for {
        k++
        if k == 2 {
            continue
        }
        fmt.Println("tick", k)
        if k >= 3 {
            break
        }
    }
}

Run:

go run for_shapes.go

Output:

table 1
table 2
table 3
left 3
left 2
left 1
tick 1
tick 3

continue skipped tick 2. break stopped at 3.

Case 2: for i := range n

Save as range_n.go. Integer range is the boring way to say “three times” without inventing a dummy slice.

// range_n.go
package main

import "fmt"

func main() {
    for i := range 3 {
        fmt.Println("cover", i)
    }
}

Run:

go run range_n.go

Output:

cover 0
cover 1
cover 2

range 0 runs zero times. A negative integer panics.

Case 3: Range a slice, then a string of runes

Save as range_slice.go. The string is "café": four runes, five bytes. The index on é is 3, not 4.

// range_slice.go
package main

import "fmt"

func main() {
    items := []string{"toast", "tea", "soup"}
    for i, item := range items {
        fmt.Println(i, item)
    }

    for i, r := range "café" {
        fmt.Printf("byte %d rune %q\n", i, r)
    }
}

Run:

go run range_slice.go

Output:

0 toast
1 tea
2 soup
byte 0 rune 'c'
byte 1 rune 'a'
byte 2 rune 'f'
byte 3 rune 'é'

If you only want values, write for _, item := range items. If you only want indexes, for i := range items.

Case 4: Labels when break is not enough

Save as label_break.go. Two nested loops: stop the outer walk when the kitchen hits a stop ticket.

// label_break.go
package main

import "fmt"

func main() {
    tables := []int{4, 7, 9}
    items := []string{"toast", "STOP", "tea"}

outer:
    for _, table := range tables {
        for _, item := range items {
            if item == "STOP" {
                fmt.Println("stop at table", table)
                break outer
            }
            fmt.Println("table", table, item)
        }
    }
    fmt.Println("done")
}

Run:

go run label_break.go

Output:

table 4 toast
stop at table 4
done

Without the label, break would only leave the inner loop and table 7 would still print. Labels are rare. Nested loops that need them are a hint to pull a function out.

The trap

Ranging a map is fine for “visit every key.” It is wrong for “print in the order we inserted” or “first key wins” if that order is a business rule. Save as map_order.go:

// map_order.go
package main

import (
    "fmt"
    "sort"
)

func main() {
    prices := map[string]int{
        "tea":   250,
        "toast": 350,
        "soup":  600,
    }

    fmt.Println("one walk:")
    for item, cents := range prices {
        fmt.Println(item, cents)
    }

    names := make([]string, 0, len(prices))
    for item := range prices {
        names = append(names, item)
    }
    sort.Strings(names)
    fmt.Println("sorted:")
    for _, item := range names {
        fmt.Println(item, prices[item])
    }
}

Run:

go run map_order.go

Possible output (the first block may shuffle; the second block is stable):

one walk:
soup 600
tea 250
toast 350
sorted:
soup 600
tea 250
toast 350

If a test asserts the first block’s order, it will flake. Sort the keys when order matters. Do not “fix” it by ranging until the order looks nice.

The boring rule

  • for is the only loop. Pick the form that names what you are walking.
  • for i := range n for a count (Go 1.22+; this book is Go 1.27).
  • for _, v := range s for elements. Keep the index when you need it.
  • Range strings for runes. Index strings for bytes (next part).
  • Never depend on map range order. Sort keys, or use a slice of keys you own.
  • Labels last. A function is clearer than break outer if the inner body is more than a few lines.

Try this

  1. In range_n.go, print i+1 so covers are 1, 2, 3.
  2. In range_slice.go, add a for i := range items loop that prints only indexes.
  3. In label_break.go, replace break outer with continue outer and explain the new output (table 7 and 9 still run until STOP).
  4. In map_order.go, run twice. Confirm the sorted block is identical and the first block may not be.