How to Write a While Loop in Go Using for

Go does not have a dedicated `while` keyword; instead, you use the `for` statement with a condition and no initialization or post statements to create a while loop.

The missing keyword

You port a script from Python or JavaScript to Go. You reach for while x < 10: or while (x < 10) { }. The editor highlights it in red. Go does not have a while keyword. It never will. Instead, Go gives you one loop statement and expects you to adapt. The language designers made a deliberate tradeoff: fewer keywords mean fewer edge cases to memorize, and a single syntax covers every iteration pattern you will ever need.

One statement, four shapes

The for keyword in Go handles everything other languages split across three or four separate keywords. It manages initialization, condition checking, post-iteration steps, and range iteration. When you strip away the initialization and post steps, you get a pure condition loop. That is your while loop. The syntax is for condition { }. The compiler evaluates the condition before the first pass. It repeats the block until the condition returns false. There is no hidden post-step. There is no implicit variable declaration. You control the entire lifecycle explicitly.

This design removes a whole class of off-by-one errors and scope confusion. In languages with separate while and for keywords, developers often mix up when to use which. Go removes the choice. You write for every time. The compiler enforces the same rules regardless of how many clauses you provide.

Minimal example

Here's the direct translation of a countdown loop.

package main

import "fmt"

func main() {
    i := 5
    // Condition only. No init or post step.
    for i > 0 {
        fmt.Println(i)
        // Decrement happens inside the block.
        i--
    }
}

The compiler evaluates i > 0 before the first iteration. If it is true, the block runs. After the block finishes, the compiler jumps back to the condition. It repeats until the condition returns false. The decrement lives inside the block because Go does not provide a post-clause in this shape. You place the state change exactly where you want it. This matches how a while loop works in C, but without the separate keyword.

How the runtime evaluates it

Go's loop evaluation is strict and predictable. The condition is checked before every single pass, including the first. If the condition starts false, the block never runs. There is no do-while behavior. If you need a guaranteed first execution, you must restructure the logic or use an unconditional loop with an early break.

Variables declared inside the loop block are scoped to that block. They are recreated on each iteration. This matters when you capture loop variables in closures. If you spawn a goroutine inside a for loop and reference the loop variable, every goroutine will see the final value unless you pass the variable as a parameter to the goroutine. The compiler used to warn about this. Go 1.22 made it a hard error. You will see loop variable i captured by func literal if you try to compile the old pattern. The fix is straightforward: pass the variable as an argument to the goroutine, or declare a new variable inside the loop with i := i.

The runtime does not optimize away condition checks. It evaluates them exactly as written. If your condition involves a function call, that function runs on every pass. This is intentional. Go prefers explicit control over hidden optimizations. You know exactly what runs and when.

Realistic example: waiting on state

Condition-only loops shine when you need to wait for an external signal. You often need to poll a flag or wait for a channel to close. Here is a polling pattern that waits for a background task.

package main

import (
    "fmt"
    "time"
)

func main() {
    ready := false
    // Spawn a background task that flips the flag after a delay.
    go func() {
        time.Sleep(2 * time.Second)
        ready = true
    }()

    // Poll the flag until the background task signals completion.
    for !ready {
        fmt.Print(".")
        // Sleep prevents CPU spinning and yields the scheduler.
        time.Sleep(100 * time.Millisecond)
    }
    fmt.Println("\nReady!")
}

The loop checks !ready on every pass. The time.Sleep call prevents the loop from burning CPU cycles. Without that sleep, the loop would spin at maximum speed, consuming an entire processor core. The background goroutine flips the flag, the next condition check sees false, and the loop exits cleanly. This pattern works for simple flags. For production code, you would typically use a channel or a sync.WaitGroup instead of polling. Polling introduces race conditions if the flag is not synchronized, and it wastes scheduler time. The loop itself is fine. The state management around it needs care.

Pitfalls and compiler feedback

Infinite loops happen when the condition never changes. Go's for { } creates an unconditional loop. It is idiomatic for server accept loops or event dispatchers. You must exit it explicitly. If you forget a break or return, the program hangs. The compiler will not stop you from writing an infinite loop. It trusts your logic. If you accidentally write for i > 0 { } without modifying i, the program runs forever. The compiler rejects syntax mistakes with straightforward messages. Writing for i > 0 without the block triggers expected '{', found 'EOF'. Forgetting to declare a variable inside the loop scope gives undefined: variable. These are standard Go errors. They point directly to the missing piece.

Another common trap is modifying the loop condition from multiple goroutines without synchronization. If two goroutines write to the flag that controls the loop, the main goroutine might read a partially updated value or miss the transition entirely. The Go memory model does not guarantee visibility across goroutines without explicit synchronization. Use channels, mutexes, or atomic operations when the condition depends on concurrent state. The loop syntax does not change. The data protection does.

Convention aside: flat control flow

Go developers rarely reach for complex loop control. The language provides break and continue. That is it. There is no goto inside loops by convention, and labeled breaks are reserved for deeply nested structures. Keep loops flat. If you need to exit early, return an error or break out. The community prefers explicit control flow over clever tricks. When you see a for loop in a codebase, expect a single exit path or a clearly labeled break. Nested loops with multiple break targets are considered a code smell. Refactor the inner logic into a function instead.

Decision matrix

Use for condition { } when you need to repeat a block until a state changes. Use for { } when you are building a long-running service or event loop that exits on explicit signals. Use for range { } when you are iterating over slices, maps, or channels. Use for init; cond; post { } when you have a clear counter that increments or decrements each pass. Use a recursive function when the problem naturally branches or when you need to unwind state after each step. Use a channel select statement when you are waiting for multiple concurrent signals instead of polling a flag.

One loop handles everything. Master the condition, and you master Go's control flow.

Where to go next