Loading
All articles
Programming 17 min read

The For Loop in Go: A Visual, Hands-On Guide

Go has one loop keyword, and it does everything. Learn the classic three-part for, the while-style and infinite forms, range over slices, strings, maps and integers, break and continue, the Go 1.22 loop variable change, common traps, real measurements, and exercises.

nbkhope
On this page

Most languages give you several loops: for, while, do-while, foreach. Go gives you exactly one keyword, for, and shapes it to fit every job: counting, repeating while a condition holds, looping forever until you break, and walking through slices, strings and maps with range.

In this guide you'll write every form of Go's for, trace a loop step by step, meet break, continue and labels, and run into the classic traps, including a bug Go itself fixed in version 1.22, which you'll reproduce by changing a single line in go.mod. Real measurements and exercises wrap it up.

All outputs below come from real runs with Go 1.22 on Windows. New to Go? Start with Your First Program in Go and How to Declare a Variable in Go.

The classic three-part for

1package main
2
3import "fmt"
4
5func main() {
6	for count := 1; count <= 5; count++ {
7		fmt.Println("Count:", count)
8	}
9	fmt.Println("Done!")
10}
1Count: 1
2Count: 2
3Count: 3
4Count: 4
5Count: 5
6Done!
The anatomy of Go's three-part for loop. for count := 1; count <= 5; count++ { … }. The init statement, count := 1, runs once before the loop starts and declares the loop variable. The condition, count <= 5, is checked before every pass: if it is true the body runs, if false the loop ends. The post statement, count++, runs after every pass. The body between the braces runs once per pass. Go uses no parentheses around the three parts, and the braces are required even for a one-line body

The header has three parts separated by semicolons:

  1. Init (count := 1): runs once, before anything else. Usually it declares the loop variable.
  2. Condition (count <= 5): checked before every pass. True runs the body; false ends the loop.
  3. Post (count++): runs after every pass.

Unlike C, C++ and Java, Go uses no parentheses around the three parts, and the braces are required, even for a one-line body.

Step by step

Tracing the loop for count := 1; count <= 5; count++. Init sets count to 1. Check 1 <= 5: true, print Count: 1, then count++ makes it 2. Check 2 <= 5: true, print Count: 2, count becomes 3. The same happens for 3, 4 and 5. Then count++ makes count 6, and the check 6 <= 5 is false, so the loop ends after 5 passes and 6 checks, and the program prints Done!. The loop variable count only exists inside the loop

The condition is checked 6 times for 5 passes: the last check (6 <= 5) is the one that stops the loop. And count exists only inside the loop; using it after the closing brace is a compile error (undefined: count).

Counting other ways

The post statement can do anything, not just add one:

1package main
2
3import "fmt"
4
5func main() {
6	for countdown := 5; countdown > 0; countdown-- {
7		fmt.Print(countdown, " ")
8	}
9	fmt.Println("Liftoff!")
10
11	for even := 0; even <= 10; even += 2 {
12		fmt.Print(even, " ")
13	}
14	fmt.Println()
15
16	for power := 1; power < 1000; power *= 2 {
17		fmt.Print(power, " ")
18	}
19	fmt.Println()
20}
5 4 3 2 1 Liftoff!
0 2 4 6 8 10 
1 2 4 8 16 32 64 128 256 512 

One keyword, four shapes

Go's one loop keyword, four shapes. 1, Three-part: for count := 1; count <= 5; count++ { }, for counting. 2, Condition only: for total < 100 { }, Go's while loop. 3, No condition: for { … break … }, loops forever until break or return. 4, Range: for index, value := range collection { }, walks through a slice, array, string, map, channel or, since Go 1.22, an integer. Go has no while or do-while keywords: for covers them all

Condition only: Go's while

Drop the init and post statements and for becomes a while loop:

1package main
2
3import "fmt"
4
5func main() {
6	savings := 100.0
7	years := 0
8	for savings < 200 {
9		savings *= 1.07
10		years++
11	}
12	fmt.Printf("Doubled to %.2f after %d years at 7%%\n", savings, years)
13}
Doubled to 210.49 after 11 years at 7%

Use this shape when you don't know in advance how many passes you'll need.

No condition: loop until break

for { } loops forever. You leave with break (or return):

1package main
2
3import "fmt"
4
5func main() {
6	number := 27
7	steps := 0
8	for {
9		if number == 1 {
10			break
11		}
12		if number%2 == 0 {
13			number /= 2
14		} else {
15			number = 3*number + 1
16		}
17		steps++
18	}
19	fmt.Println("Reached 1 after", steps, "steps")
20}
Reached 1 after 111 steps

That's the famous Collatz sequence: halve even numbers, triple-and-add-one odd ones, and (as far as anyone has checked) you always reach 1. Starting at 27 takes a surprising 111 steps. A for { } is also how servers and games run their main loops.

range: walking through collections

Slices

1package main
2
3import "fmt"
4
5func main() {
6	fruits := []string{"apple", "banana", "cherry"}
7	for index, fruit := range fruits {
8		fmt.Println(index, fruit)
9	}
10	for _, fruit := range fruits {
11		fmt.Print(fruit, " ")
12	}
13	fmt.Println()
14	for index := range fruits {
15		fmt.Print(index, " ")
16	}
17	fmt.Println()
18}
10 apple
21 banana
32 cherry
4apple banana cherry 
50 1 2 

range gives you two values per pass: the index and a copy of the element. Don't need the index? Write _ (the blank identifier), because an unused variable is a compile error. Need only the index? Leave the second variable out.

Integers (Go 1.22)

Since Go 1.22, range works on a plain integer, counting from 0 up to (but not including) it:

1package main
2
3import "fmt"
4
5func main() {
6	for lap := range 3 {
7		fmt.Println("Lap", lap+1)
8	}
9}
Lap 1
Lap 2
Lap 3

for lap := range 3 is a tidier way to write for lap := 0; lap < 3; lap++.

Strings: characters, not bytes

1package main
2
3import "fmt"
4
5func main() {
6	for index, character := range "Olá!" {
7		fmt.Printf("byte %d: %c\n", index, character)
8	}
9}
1byte 0: O
2byte 1: l
3byte 2: á
4byte 4: !
Ranging over a string. The string "Olá!" is 5 bytes in UTF-8: O at byte 0, l at byte 1, á taking bytes 2 and 3, and ! at byte 4. A for range loop gives one rune, one whole character, per pass, together with the index of its first byte: 0 O, 1 l, 2 á, 4 !. The index jumps from 2 to 4 because á is two bytes. A classic loop with s[i] would visit 5 bytes instead and split the á in half

Ranging over a string gives you one rune (a whole Unicode character) per pass, plus the byte index where it starts. That's why the index jumps from 2 to 4: á takes two bytes. (%c prints a rune as a character.) A classic index loop with text[index] would visit bytes instead and cut á in half.

Maps: in no particular order

1package main
2
3import (
4	"fmt"
5	"sort"
6)
7
8func main() {
9	capitals := map[string]string{"France": "Paris", "Japan": "Tokyo", "Brazil": "Brasília", "Kenya": "Nairobi"}
10
11	countries := make([]string, 0, len(capitals))
12	for country := range capitals {
13		countries = append(countries, country)
14	}
15	sort.Strings(countries)
16
17	for _, country := range countries {
18		fmt.Println(country, "→", capitals[country])
19	}
20}
1Brazil → Brasília
2France → Paris
3Japan → Tokyo
4Kenya → Nairobi

Ranging over a map gives each key and value, but the order is deliberately random, so programs can't come to depend on it. A plain for country, capital := range capitals { fmt.Println(country, capital) } printed this on two runs in a row:

1$ go run .
2Japan Tokyo
3Brazil Brasília
4Kenya Nairobi
5France Paris
6$ go run .
7France Paris
8Japan Tokyo
9Brazil Brasília
10Kenya Nairobi
How random is map iteration? We ranged over the same 4-key map 10,000 times in one Go 1.22 program and recorded the order. Only 4 different orders appeared, not all 24 possible orders: France, Japan, Brazil, Kenya: 6,235 times; Kenya, France, Japan, Brazil: 1,275 times; Japan, Brazil, Kenya, France: 1,248 times; Brazil, Kenya, France, Japan: 1,242 times. Go picks a random starting point and walks the map's internal buckets from there, so the orders are rotations. Never rely on map order: when order matters, sort the keys first

We ranged over that map 10,000 times and counted the orders: only 4 distinct orders appeared (rotations of one sequence, because Go picks a random starting point), and one of them came up 62% of the time. Either way: when order matters, collect the keys and sort them, as above.

break, continue and labels

1package main
2
3import "fmt"
4
5func main() {
6	for number := 1; number <= 10; number++ {
7		if number%3 == 0 {
8			continue
9		}
10		if number > 8 {
11			break
12		}
13		fmt.Print(number, " ")
14	}
15	fmt.Println()
16
17	target := 12
18search:
19	for first := 1; first <= 6; first++ {
20		for second := 1; second <= 6; second++ {
21			if first*second == target {
22				fmt.Printf("%d x %d = %d\n", first, second, target)
23				break search
24			}
25		}
26	}
27}
1 2 4 5 7 8 
2 x 6 = 12
  • continue skips the rest of this pass and moves on to the next one (3 and 6 are skipped).
  • break leaves the loop entirely (at 9).
  • Labels: a plain break only leaves the innermost loop. Put a label like search: before the outer loop and break search leaves both at once, so we stop at the first pair (2 × 6) instead of also finding 3 × 4, 4 × 3 and 6 × 2.

A loop or a formula?

Loops are the obvious way to add up 1 + 2 + … + n:

1package main
2
3import "fmt"
4
5func main() {
6	limit := 100
7	total := 0
8	for number := 1; number <= limit; number++ {
9		total += number
10	}
11	fmt.Println("Loop:", total)
12	fmt.Println("Formula:", limit*(limit+1)/2)
13}
Loop: 5050
Formula: 5050

As a schoolboy, Carl Friedrich Gauss reportedly spotted the shortcut: pair 1 with 100, 2 with 99 and so on, 50 pairs of 101. We timed both for n = 10 million:

Loop speed in Go, measured with testing.Benchmark on Go 1.22, Windows. Summing a slice of 10 million ints: an index loop took 5.2 milliseconds and a range loop 5.6 milliseconds, the same within measurement noise; whichever ran first was faster, so the order mattered more than the style. Adding up 1 to 10 million with a loop took 3.9 milliseconds; the formula n times (n plus 1) divided by 2 took 0.7 nanoseconds, about 5 million times faster. Loops are fast, about half a nanosecond per number, but a better algorithm beats any loop
WhatTime
Sum 10 million slice elements, index loop5.2 ms
Sum 10 million slice elements, range loop5.6 ms
Add up 1 to 10 million with a loop3.9 ms
Add up 1 to 10 million with the formula0.7 ns

Two lessons. Index loops and range loops run at the same speed (whichever we ran first came out ahead, so the gap is noise), so pick the clearer one, usually range. And a loop doing half a nanosecond of work per number is fast, but a better idea is millions of times faster.

Traps

Trap 1: range gives you a copy

1package main
2
3import "fmt"
4
5func main() {
6	prices := []int{10, 20, 30}
7	for _, price := range prices {
8		price *= 2
9	}
10	fmt.Println(prices)
11
12	for index := range prices {
13		prices[index] *= 2
14	}
15	fmt.Println(prices)
16}
[10 20 30]
[20 40 60]

The price variable is a copy of each element, so changing it changes nothing in the slice. To modify elements, assign through the index.

Trap 2: loop variables in closures (fixed in Go 1.22)

This program saves three functions in a loop and calls them afterward:

1package main
2
3import "fmt"
4
5func main() {
6	var printers []func()
7	for i := 0; i < 3; i++ {
8		printers = append(printers, func() { fmt.Print(i, " ") })
9	}
10	for _, printer := range printers {
11		printer()
12	}
13	fmt.Println()
14}
0 1 2 

That's what you'd expect, and it's what Go 1.22 and later print. But the go line in your go.mod decides which loop rules apply. Change it from go 1.22 to go 1.21 and run the very same code:

$ go run .
3 3 3 
The Go 1.22 loop variable change. With go 1.21 in go.mod, a for loop had one loop variable shared by every pass: the three saved functions all point at the same i, which is 3 when they run after the loop, so they print 3 3 3. With go 1.22 or later in go.mod, each pass gets its own fresh copy of the loop variable: the functions keep 0, 1 and 2, and print 0 1 2. Both results are real runs of the same code with Go 1.22.5, changing only the go line in go.mod. Goroutines started in a loop had the same bug, one of the most common in Go's history

Before Go 1.22, the loop had one i shared by every pass, so all three functions saw its final value, 3. The same bug bit goroutines started inside loops, and it was one of the most common mistakes in Go code, so Go 1.22 changed the rules: each pass now gets its own variable. (The i here is the plain loop counter of a short example; in longer loops, prefer descriptive names.) If you maintain older code, check its go.mod version.

Trap 3: off by one

for index := 0; index <= len(fruits); index++ runs one pass too many and crashes with panic: runtime error: index out of range [3] with length 3. Valid indexes go from 0 to len - 1, so the condition is <, not <=. Better still, use range and let Go handle the bounds.

Your turn: practice

Try these before peeking at the answers. Run each with go run ..

Exercise 1: Predict the output (warm-up). What does this print?

1package main
2
3import "fmt"
4
5func main() {
6	for number := 10; number > 0; number -= 3 {
7		fmt.Print(number, " ")
8	}
9	fmt.Println()
10	for index, letter := range "Go!" {
11		fmt.Print(index, string(letter), " ")
12	}
13	fmt.Println()
14}

Exercise 2: Multiplication table. Print the 7 times table from 7 × 1 to 7 × 10, one line each, like 7 x 3 = 21.

Exercise 3: Sum the evens. Add up the even numbers from 1 to 100 using continue to skip the odd ones.

Exercise 4: FizzBuzz. For the numbers 1 to 15, print Fizz for multiples of 3, Buzz for multiples of 5, FizzBuzz for multiples of both, and the number otherwise, all on one line.

Exercise 5: Count the vowels (challenge). Count the vowels (a, e, i, o, u, including accented ones like á and ê) in "A programação é divertida" using range over the string and strings.ContainsRune.

Quick quiz

  1. How many loop keywords does Go have?
  2. How do you write a while loop in Go?
  3. What two values does range give you over a slice?
  4. Why does range over "Olá" skip from index 2 to 4?
  5. What changed about loop variables in Go 1.22?

Answers

Exercise 1: The first loop counts down by 3 until the number isn't greater than 0; the second gives byte indexes and characters:

1package main
2
3import "fmt"
4
5func main() {
6	for number := 10; number > 0; number -= 3 {
7		fmt.Print(number, " ")
8	}
9	fmt.Println()
10	for index, letter := range "Go!" {
11		fmt.Print(index, string(letter), " ")
12	}
13	fmt.Println()
14}
10 7 4 1 
0G 1o 2! 

Exercise 2: range 10 counts 0 to 9, so add 1:

1package main
2
3import "fmt"
4
5func main() {
6	for step := range 10 {
7		multiplier := step + 1
8		fmt.Printf("7 x %d = %d\n", multiplier, 7*multiplier)
9	}
10}
17 x 1 = 7
27 x 2 = 14
37 x 3 = 21
47 x 4 = 28
57 x 5 = 35
67 x 6 = 42
77 x 7 = 49
87 x 8 = 56
97 x 9 = 63
107 x 10 = 70

Exercise 3: continue skips odd numbers before they're added:

1package main
2
3import "fmt"
4
5func main() {
6	total := 0
7	for number := 1; number <= 100; number++ {
8		if number%2 != 0 {
9			continue
10		}
11		total += number
12	}
13	fmt.Println(total)
14}
2550

Exercise 4: Check "both" first, or 15 would stop at Fizz:

1package main
2
3import "fmt"
4
5func main() {
6	for number := 1; number <= 15; number++ {
7		switch {
8		case number%15 == 0:
9			fmt.Print("FizzBuzz ")
10		case number%3 == 0:
11			fmt.Print("Fizz ")
12		case number%5 == 0:
13			fmt.Print("Buzz ")
14		default:
15			fmt.Print(number, " ")
16		}
17	}
18	fmt.Println()
19}
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz 

(A switch with no value checks each case condition in order, a tidy replacement for an if/else if chain.)

Exercise 5: range over the string gives whole characters, so á and é count once each:

1package main
2
3import (
4	"fmt"
5	"strings"
6)
7
8func main() {
9	text := "A programação é divertida"
10	vowels := "aeiouáéíóúâêôãõAEIOU"
11	count := 0
12	for _, character := range text {
13		if strings.ContainsRune(vowels, character) {
14			count++
15		}
16	}
17	fmt.Println("Vowels:", count)
18}
Vowels: 11

Quiz: (1) One: for. (2) With only a condition: for total < 100 { … }. (3) The index and a copy of the element. (4) range over a string yields runes with their starting byte index, and á takes two bytes. (5) Each pass of the loop now gets its own copy of the loop variable, so closures and goroutines created in the loop see the value from their own pass (when go.mod says 1.22 or later).

Cheat sheet

I want to…Write
Count from 1 to 10for count := 1; count <= 10; count++ { }
Repeat n times (Go 1.22+)for step := range n { }
Loop while a condition holdsfor total < 100 { }
Loop until breakfor { … if done { break } … }
Walk a slicefor index, value := range items { }
Values only / indexes onlyfor _, value := range items / for index := range items
Walk a string's charactersfor index, character := range text { }
Walk a map (any order)for key, value := range table { }
Skip to the next passcontinue
Leave the loopbreak
Leave nested loopslabel the outer loop, then break label
Change slice elementsitems[index] = …, not the range copy

Wrapping up

Go's single for keyword covers every kind of loop:

  • Three parts (init; condition; post) for counting, with no parentheses and required braces.
  • Condition only is Go's while; no condition loops until break.
  • range walks slices (index and a copy), strings (runes and byte indexes), maps (in random order) and, since Go 1.22, integers.
  • continue skips a pass, break leaves, and labels break out of nested loops.
  • Since Go 1.22, each pass gets its own loop variable, fixing the classic closure bug (the same code printed 3 3 3 with go 1.21 in go.mod).
  • Index and range loops are equally fast, about half a nanosecond per element in our test, but the right formula beat the loop by millions of times.

Back through the series: How to Declare a Variable in Go, How to Install Go on Linux and Your First Program in Go. Next up: two-dimensional slices in Go.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.