The for Statement

Complete guide to Go's only looping construct - the for statement - covering three-clause loops, condition-only loops, infinite loops, scope rules, and common patterns

Go has exactly one looping keyword: for. This design choice eliminates the need for separate while or do-while keywords and unifies every repeating code pattern under a single, familiar construct. A for loop can count iterations, repeat while a condition holds, run forever, or iterate over collections with range (which is covered in the next section). This document explains every form of the for statement, how the loop mechanics work under the hood, and what beginners most often get wrong.

The Three-Clause Form

The classic counted loop uses all three clauses separated by semicolons. It matches the mental picture most programmers already have: initialise a counter, check a boundary, and update the counter after each pass.

package main
import "fmt"
func main() {
    sum := 0
    for i := 0; i < 5; i++ {
        sum += i
    }
    fmt.Println("Sum:", sum) // Sum: 10
}

The loop runs in this order:

  1. The init statement i := 0 executes once, before any iteration. It declares i with short variable declaration, so i is scoped only to the for block.
  2. The condition i < 5 is evaluated before every iteration, including the first. If it’s false, the loop body never runs.
  3. The body sum += i executes when the condition is true.
  4. The post statement i++ runs immediately after the body, then control jumps back to step 2.

Scope of init variables:

Variables declared in the init statement are confined to the for loop’s implicit block. They do not exist outside the loop, and using them after the closing brace is a compile-time error.

What Happens When Clauses Are Missing

All three clauses are optional, but the semicolons still tell the compiler which role each piece plays. You can omit the init or post while keeping the semicolons in place:

i := 0      // i is declared outside the loop scope
for ; i < 5; i++ {
    fmt.Println(i)
}
fmt.Println("Final i:", i) // i is still visible here

Omitting the condition entirely — for example, writing for ; ; { } — is equivalent to for true { }, but the Go community prefers the shorter for { } form for infinite loops. Leaving out the init or post is most useful when a variable is already initialised outside the loop or when the loop body handles the update itself.

Semicolons are easy to forget:

Writing for i < 5 { } is legal and acts as a condition-only loop. Writing for i := 0; i < 5 { } without the trailing semicolon is a syntax error. The compiler needs both semicolons to know you are using the three-clause form.

Condition-Only Loops (While-Style)

When you drop the init and post statements along with the semicolons, you get a loop that behaves exactly like a while loop in other languages. The condition is checked before every iteration, so the body may execute zero times.

package main
import "fmt"
func main() {
    num := 1
    max := 10
    for num <= max {
        fmt.Println(num)
        num *= 2
    }
}

Here the loop doubles num each time. On the first pass num is 1, then 2, 4, 8. When num becomes 16 the condition 16 <= 10 is false, and the loop stops. The pattern is common anytime the number of iterations is unknown before the loop starts — reading input until EOF, retrying an operation, or walking a linked list.

A condition-only loop is just the three-clause form with the init and post left blank, so the same scope rule applies: variables declared inside the loop body are scoped to that iteration, not visible across iterations unless declared outside.

Recognising the pattern:

If you ever catch yourself writing for true { } in Go, stop and write for { } instead. The compiler treats them identically, but the bare for is the canonical way to express “run forever until explicitly stopped.”

Infinite Loops

A for loop with no condition runs indefinitely. The only way out is a break, a return, or a panic. This form is the backbone of servers, event loops, and any program that needs to stay alive until externally killed.

package main
import (
    "fmt"
    "time"
)
func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    defer ticker.Stop()
    for {
        select {
        case t := <-ticker.C:
            fmt.Println("Tick at", t)
        }
    }
}

The loop above never exits on its own; a SIGINT or other external signal would stop the process. For command-line tools or finite tasks, you often pair an infinite loop with a break that triggers when a sentinel value appears or a counter reaches a limit.

An infinite loop without an exit path freezes your program:

If you write for { } (or for { fmt.Println("looping") }) with no break, return, or panic, the goroutine running it will loop forever and can never be preempted by the scheduler at that same point. This is not a background task — it’s a hang. Always ensure there is a reachable termination condition.

Loop Variable Scoping in Detail

Variables declared with a short declaration in the init statement belong to a new block that encloses the entire for statement — not just the body. Consider this snippet:

for i := 0; i < 3; i++ {
    fmt.Println(i)
}
fmt.Println(i) // compile error: undefined: i

i does not leak out. This is deliberate: it prevents accidental reuse of a loop variable after the loop finishes and makes the lifetime of the variable obvious.

If you need the final value of a counter after the loop, declare it before the for block:

var i int
for i = 0; i < 3; i++ {
    fmt.Println(i)
}
fmt.Println("final i:", i) // prints 3

A subtle point: each iteration does not create a new variable for the init-declared variables. The same variable is reused across iterations, which matters when closures or goroutines capture it. The range clause changes this behaviour (covered later), but for classic for loops, the variable is reused.

Closures capture the same loop variable:

Launching goroutines inside a for i := 0; i < 5; i++ loop and using i in the goroutine body will likely print the last value of i multiple times. Capture the value by passing it as an argument or creating a local copy: i := i inside the loop body.

Branching Inside for Loops: break and continue

break exits the innermost for loop immediately. continue skips the rest of the current iteration’s body and jumps directly to the post statement (if any), then the condition check.

for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // skip even numbers
    }
    if i > 7 {
        break // stop early
    }
    fmt.Println("odd:", i)
}
// Output:
// odd: 1
// odd: 3
// odd: 5
// odd: 7

Labels can direct break and continue to a specific enclosing loop when loops are nested. That topic is detailed in the Labels section, but the syntax is straightforward: you prefix the outer for with a label, then write break LabelName.

Common Patterns

The for statement shows up in a small set of recurring shapes. Recognising them helps you read and write Go code faster.

Fixed-count iteration

for i := 0; i < n; i++ {
    process(i)
}

This is the most frequent form. Use it when you know exactly how many times the loop must run.

Sentinel-controlled loop

var input string
for input != "quit" {
    fmt.Scan(&input)
    handle(input)
}

The loop stops when a special value appears. This is a condition-only loop.

Loop-and-a-half (read until done)

for {
    item, ok := next()
    if !ok {
        break
    }
    process(item)
}

The infinite loop with an early exit in the middle handles scenarios where you can’t test the termination condition until partway through the iteration body. This pattern avoids duplicating the call to next().

Loop over a collection (using index)

for i := 0; i < len(items); i++ {
    fmt.Println(items[i])
}

While range is the idiomatic way to walk slices and arrays, an index-based loop is useful when you need to modify the slice in place or access elements by position relative to i.

Common Mistakes and How to Avoid Them

Off-by-one errors The condition i <= len(arr) will panic with an index out of range because Go slice indices go from 0 to len(arr)-1. The correct condition is i < len(arr). Train yourself to write strict less-than in counted loops over collections.

Assignment = instead of comparison ==

for i := 0; i = 5; i++ { // compile error: i = 5 used as value

Go catches this at compile time because i = 5 is a statement, not an expression that produces a boolean. But in a condition-only loop, for x = 10 { } is illegal, so the compiler protects you.

Forgetting to increment the counter

for i := 0; i < 5; {
    fmt.Println(i)
    // missing i++
}

The loop body never changes i, so the condition stays true forever. This compiles but hangs. Use the post statement to keep the update visible near the condition.

Infinite loop without an exit A for { } with no break, return, or panic will block the goroutine forever. Even an empty infinite loop chews CPU. Always have a termination plan.

Accidentally reusing a loop variable after the loop The compiler prevents you from using an init-declared variable outside the loop, but if you declare the variable before the loop, ensure you reset it appropriately when reusing the same variable in another loop.

How Beginners Should Think About for Loops

Think of a for loop as a guard at a door. The guard checks a condition; if it passes, you walk through the door, do some work, and come back to the guard who checks the condition again. The init statement sets up whatever the guard needs to make the first check (like a counter starting at zero). The post statement changes something so the guard might eventually say “no” and stop letting you through. Without the post statement, the guard will keep letting you in forever unless something inside the work you do causes an exit.

When you write for condition { }, you are telling the guard: “Let me in as long as this condition holds, but I’ll manage the changes myself inside.”

When you write for { }, you are saying: “I will handle everything — I’ll do the work and find my own way out.” This mental model makes it easier to see why a missing increment is dangerous and why break is the only polite way to leave an infinite loop.

Summary

Go’s for statement absorbs the responsibilities of traditional for, while, and do-while loops while keeping the syntax minimal. The three-clause form provides precise control over counting loops; the condition-only form acts as a pre-tested while loop; and the bare for creates intentional infinite loops that must be terminated explicitly. Variable scoping rules prevent accidental leakage, and break/continue work predictably across all forms.