Loops
Go has one loop construct: for. The keyword serves four roles via different clause forms — three-clause C-style (for init; cond; post), condition-only (for cond), infinite (for), and range-based (for ... range). The unification eliminates the C-family while/do-while/for distinction. Like other Go control constructs, for requires braces and admits no parenthesised header. The combination — single keyword, mandatory braces, four clause forms, range over slices/maps/strings/channels — covers the iteration surface.
The four for forms
Three-clause form
for i := 0; i < 10; i++ {
fmt.Println(i)
}
The conventional C-style: an initialisation statement, a condition, and a post statement. Each part is optional but the semicolons are required if any are present.
Condition-only form
for n > 0 {
n--
fmt.Println(n)
}
The form is Go’s while — runs while the condition is true.
Infinite form
for {
// ...
if done {
break
}
}
The form is Go’s while (true) — runs forever until break, return, or panic.
Range form
v := []int{10, 20, 30}
for i, x := range v {
fmt.Printf("[%d] = %d\n", i, x)
}
The form admits iterating over slices, arrays, maps, strings, and channels. Treated below.
break and continue
for i := 0; i < 10; i++ {
if i == 5 {
break // exit the loop
}
if i % 2 == 0 {
continue // skip to next iteration
}
fmt.Println(i) // 1, 3
}
break exits the innermost enclosing for, switch, or select; continue proceeds to the next iteration.
Labelled break and continue
For nested loops, labels admit targeting a specific loop:
outer:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i*j > 50 {
break outer // breaks the outer loop
}
}
}
next:
for _, line := range lines {
for _, word := range strings.Fields(line) {
if word == "skip" {
continue next // proceeds to next line
}
process(word)
}
}
The mechanism is occasionally needed; conventionally, restructuring into a function with return is clearer.
The range form
range produces (index, value) pairs for slices/arrays, (key, value) pairs for maps, (index, rune) pairs for strings, and value-only for channels:
Range over a slice
v := []string{"a", "b", "c"}
for i, x := range v { // index and value
fmt.Printf("[%d] = %s\n", i, x)
}
for _, x := range v { // value only
fmt.Println(x)
}
for i := range v { // index only
v[i] = strings.ToUpper(v[i])
}
The blank identifier _ admits discarding the index. The single-receiver form receives the index alone.
Range over an array
Same as slices:
arr := [3]int{1, 2, 3}
for i, x := range arr {
fmt.Printf("[%d] = %d\n", i, x)
}
A subtle point: range over an array iterates over the value (a copy), not a reference. To modify, range over a pointer to the array or use a slice.
Range over a map
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m { // key and value
fmt.Printf("%s = %d\n", k, v)
}
for k := range m { // key only
fmt.Println(k)
}
The iteration order is randomised — Go deliberately produces different orders across runs to discourage code that depends on map order. For ordered iteration, sort the keys:
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])
}
Range over a string
s := "héllo"
for i, r := range s { // byte index and rune
fmt.Printf("byte %d: rune %c\n", i, r)
}
The mechanism produces the rune sequence (Unicode code points), with i being the byte index of each rune (since runes may be multi-byte in UTF-8).
For byte iteration, use the indexed form:
for i := 0; i < len(s); i++ {
fmt.Printf("byte %d: %d\n", i, s[i])
}
Range over a channel
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}()
for v := range ch { // receive until channel is closed
fmt.Println(v)
}
The form receives values until the channel is closed; if not closed, it blocks indefinitely.
Range over an integer (Go 1.22+)
for i := range 10 {
fmt.Println(i) // 0, 1, ..., 9
}
The form is a recent addition; it admits “loop n times” without the explicit three-clause form.
Common patterns
Iterating with index
for i, x := range slice {
fmt.Printf("[%d] = %v\n", i, x)
}
Iterating values only
for _, x := range slice {
process(x)
}
Iterating in reverse
Slices admit reverse iteration via the indexed form:
for i := len(slice) - 1; i >= 0; i-- {
fmt.Println(slice[i])
}
Modifying during iteration
// Index-based: safe
for i := range slice {
slice[i] = transform(slice[i])
}
// Range-by-value: the value is a COPY; modifying x doesn't affect slice
for _, x := range slice {
x = transform(x) // does NOT modify slice
}
For modifying, use the indexed form. For appending during iteration, range captures the slice’s length at the start; new appends are not visited:
v := []int{1, 2, 3}
for i, x := range v {
v = append(v, x*2) // NOT iterated
if i > 5 { break }
}
// v is [1, 2, 3, 2, 4, 6]; only the original 3 elements were iterated
Loop with state
state := initialState()
for !state.IsTerminal() {
state = state.Step()
}
Polling
for {
if condition() {
break
}
time.Sleep(100 * time.Millisecond)
}
For condition-driven loops with no upfront iteration count, the infinite-for form is conventional.
Reading lines from stdin
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
process(line)
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
The pattern is the conventional Go form for reading input line by line.
Range over a generated sequence (Go 1.23+)
The range form admits iterator functions since Go 1.23:
import "iter"
func evens(max int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < max; i += 2 {
if !yield(i) {
return
}
}
}
}
for n := range evens(10) {
fmt.Println(n) // 0, 2, 4, 6, 8
}
The mechanism is recent; the conventional form for routine iteration remains slice/map/channel range.
Worker loop
func worker(jobs <-chan Job, results chan<- Result) {
for job := range jobs { // receive until channel closes
results <- process(job)
}
}
The pattern is conventional in goroutine-based worker pools.
Retry loop
const maxAttempts = 5
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
result, err := operation()
if err == nil {
return result, nil
}
lastErr = err
time.Sleep(backoff(attempt))
}
return Result{}, fmt.Errorf("after %d attempts: %w", maxAttempts, lastErr)
Dual-channel select loop
for {
select {
case msg := <-messages:
process(msg)
case <-quit:
return
}
}
Treated in Concurrency.
Skip the rest with continue
for _, item := range items {
if !item.Valid() {
continue // skip invalid
}
process(item)
}
Break on error
for _, line := range lines {
if err := process(line); err != nil {
log.Printf("processing failed: %v", err)
break
}
}
A note on the absence of while and do-while
Go does not have separate while and do-while. The two are expressed via for:
// while:
for cond {
body
}
// do-while equivalent:
for {
body
if !cond { break }
}
The unification simplifies the language at the cost of slight verbosity in the do-while case.
A note on the conventional discipline
The contemporary Go loop advice:
- Use
for ... rangefor iteration; it’s the conventional form. - Use the three-clause form for indexed iteration with explicit step.
- Use the condition-only form (
for cond) forwhile-style loops. - Use the infinite form (
for {}) for retry loops, polling, event loops. - Use
breakandcontinuefreely. - Use labels rarely — restructure when nested-loop control is needed.
- Use the blank identifier (
_) to discard the index. - Sort keys for deterministic map iteration.
The combination — single for keyword, four clause forms, range over slices/maps/strings/channels, mandatory braces — is the substance of Go’s iteration surface. The discipline produces clear, explicit looping code.