Loops and Iteration
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.goPossible 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
foris the only loop. Pick the form that names what you are walking.for i := range nfor a count (Go 1.22+; this book is Go 1.27).for _, v := range sfor 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 outerif the inner body is more than a few lines.
Try this
- In
range_n.go, printi+1so covers are 1, 2, 3. - In
range_slice.go, add afor i := range itemsloop that prints only indexes. - In
label_break.go, replacebreak outerwithcontinue outerand explain the new output (table 7 and 9 still run until STOP). - In
map_order.go, run twice. Confirm the sorted block is identical and the first block may not be.