Go has exactly one loop keyword: for. It has four shapes, and between them they cover every loop you would write with for, while, do while or foreach in other languages.
No parentheses go around the loop header, and braces are always required.
The Three-Clause Loop
for init; condition; post {
// body
}
initruns once before the loop. It is usually a short variable declaration likei := 0, and that variable exists only inside the loop.conditionis checked before every iteration. When it is false, the loop ends.- The body runs.
postruns after each iteration, typicallyi++.
i++ is a statement in Go, not an expression, so post cannot contain i++, j--. Use a parallel assignment instead: i, j = i+1, j-1.
All three clauses are optional. for ; i < 10; {} is legal, and gofmt rewrites it to for i < 10 {}, which is the next form.
Condition-Only Loop (Go's while)
With just a condition, for behaves like while in other languages:
There is no separate do while. The while loop page shows how to get "run at least once" behavior.
Infinite Loops
for with nothing after it loops forever. Leave it with break, return, os.Exit or a panic:
for {
line, err := reader.ReadString('\n')
if err != nil {
break
}
process(line)
}
This is the standard shape for servers, workers and input loops. A for {} with an empty body burns a CPU core; in real code, the body blocks on something (a read, a channel receive, a select).
range Loops
range iterates over a collection and yields one or two values per iteration. What they are depends on what you range over:
| Range over | First value | Second value |
|---|---|---|
| slice or array | index | element (a copy) |
| string | byte index | rune (Unicode character) |
| map | key | value |
| channel | element | (none) |
integer n (Go 1.22) | 0 to n minus 1 | (none) |
| iterator function (Go 1.23) | what the function yields | what the function yields |
Three details the output shows:
- The value variable is a copy.
v *= 2inside the loop would not change the slice; to modify elements, index into the slice withnums[i], as the third loop does. - A string's index is a byte offset.
étakes two bytes, so the index jumps from 1 to 3. - Map order is random on purpose. Go randomizes it so code cannot depend on it.
slices.Sorted(maps.Keys(m))(Go 1.23) gives the keys in order.
range is covered in full, including channels, iterator functions and the copying rules, on the range page.
break and continue
break ends the innermost loop. continue skips to the next iteration (running post first, in a three-clause loop):
Labeled break and continue
A plain break only leaves the innermost loop. To exit an outer loop from inside a nested one, label the outer loop:
A label is an identifier followed by a colon, placed directly before the for. Go reports an unused label as a compile error, just like an unused variable.
break inside switch and select
Inside a for, a break in a switch or select case leaves the switch or select, not the loop:
for {
switch cmd := next(); cmd {
case "quit":
break // only exits the switch; the loop keeps running
}
}
Use a labeled break, or return, to leave the loop from inside a switch or select.
Loop Variables Since Go 1.22
Go 1.22 changed the scope of loop variables: each iteration now gets a fresh copy of i and v. Before, one variable was reused for the whole loop, which broke closures and goroutines that captured it:
With Go 1.22 and later this prints 0 1 2 and [aa bb cc]. Under the old semantics the same program written with for i := 0; i < 3; i++ (range over an integer did not exist before 1.22) printed 3 3 3, and the goroutines raced on the shared i and name. The old workaround, i := i at the top of the loop body, is no longer needed.
The behavior is chosen per module by the go line in go.mod, not by the compiler version. A module that still says go 1.21 keeps the old semantics even when built with Go 1.24.
Common Mistakes
Modifying the copy. for _, v := range items { v.Count++ } changes a copy. Use items[i].Count++, or range over a slice of pointers.
Appending while ranging. range evaluates the slice once, at the start. Elements appended inside the loop are not visited, so the loop still terminates, but it will not see the new items.
Deleting from a slice by index in a forward loop. Removing element i shifts the rest left, so the next element gets skipped. Build a new slice, loop backwards, or use slices.DeleteFunc.
Off-by-one with <=. for i := 0; i <= len(s); i++ reads s[len(s)] on the last pass and panics with index out of range. Use <, or better, range.
Heavy work in the condition. The condition runs every iteration. for i := 0; i < expensiveCount(); i++ calls the function each time; compute it once before the loop.
Using a float as a counter. for x := 0.0; x != 1.0; x += 0.1 never ends, because 0.1 has no exact binary representation. Count with an integer and compute the float from it.
Frequently Asked Questions
How do you write a for loop in Go?
The classic form has three parts separated by semicolons and no parentheses:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
Since Go 1.22 you can also write for i := range 5, which counts from 0 to 4. To walk a slice, use for i, v := range items.
Does Go have a while loop?
No while keyword, but for with only a condition is a while loop: for n > 0 { n /= 2 }. for { ... } with no condition is an infinite loop that you leave with break or return.
How do I loop over a map in Go?
for key, value := range m { ... }. The iteration order is not specified, and the runtime randomizes it so it can differ from one loop to the next. Never rely on it. For a stable order, collect the keys, sort them with slices.Sort, and loop over the sorted keys.
How do I break out of a nested loop in Go?
Put a label before the outer loop and name it in the break: outer: for ... { for ... { if found { break outer } } }. A plain break only leaves the innermost for, switch or select. continue outer likewise jumps to the next iteration of the labeled loop.
What changed about loop variables in Go 1.22?
Before Go 1.22, a for loop had one variable shared by every iteration, so closures and goroutines started in the loop often all saw the final value. Since Go 1.22 each iteration gets its own copy, so capturing i or v in a closure works as expected. The new behavior applies when the module's go.mod says go 1.22 or later.