The only loop in Go
You write a background service. It needs to keep running until an operator tells it to shut down. You write a message queue consumer. It needs to keep pulling jobs until the system drains or the process dies. In most languages, you reach for while(true) or do { } while(true). Go gives you exactly one loop keyword: for. Strip away the initialization, condition, and post-statement, and you get for { }. That is the infinite loop. It looks sparse. It behaves exactly how you expect.
Go's designers removed while and do-while to keep the language small and predictable. One loop keyword means one mental model. You do not have to remember which loop checks the condition before the body and which checks it after. for always checks before. Remove the condition, and the check always passes. The loop runs until you tell it to stop. Think of for { } as an empty conveyor belt. The belt keeps turning until you manually pull the emergency stop.
One loop keyword means one mental model. Remove the condition, and the check always passes.
The minimal infinite loop
Here is the simplest infinite loop: spawn it, count up, and break when a threshold is reached.
package main
import "fmt"
func main() {
// Track iterations outside the loop body so the value survives each pass
count := 0
// Omitting the condition makes the loop evaluate to true on every check
for {
count++
// Print the current state before evaluating the exit condition
fmt.Printf("Iteration: %d\n", count)
// Break exits the innermost loop immediately when the threshold is met
if count >= 5 {
fmt.Println("Stopping loop.")
break
}
}
}
When the compiler sees for { }, it generates a jump instruction that points back to the top of the block. There is no hidden condition check at runtime. The CPU just executes the body, hits the closing brace, and jumps back up. The only way out is a control flow statement: break, return, goto, or panic. If none of those execute, the goroutine runs forever.
The break statement only exits the innermost loop. If you nest loops, break will not unwind the outer one. You can attach a label to the outer loop and pass it to break, but that pattern rarely appears in idiomatic Go. Most developers flatten nested loops into separate functions or use return to exit the entire function at once. The compiler will reject your program with break statement outside of loop if you try to use it without a surrounding loop block.
The CPU just jumps back up. The only way out is a control flow statement.
Real-world pattern: listening forever
Production code rarely runs forever without a way to shut down gracefully. Here is a background worker that processes messages until a context signals cancellation.
package main
import (
"context"
"fmt"
)
// Worker processes items from a channel until the context is cancelled
func Worker(ctx context.Context, jobs <-chan string) {
// The infinite loop keeps the goroutine alive to receive work
for {
// Select waits on multiple channels without blocking the goroutine permanently
select {
case job, ok := <-jobs:
// The channel was closed, so stop processing and return
if !ok {
fmt.Println("Job channel closed. Exiting.")
return
}
fmt.Printf("Processing: %s\n", job)
case <-ctx.Done():
// The context received a cancellation signal, so bail out immediately
fmt.Println("Context cancelled. Shutting down.")
return
}
}
}
This pattern relies on select. A select statement blocks until one of its channel operations can proceed. If multiple are ready, Go picks one at random. By pairing for { } with select, you create a non-blocking event loop. The goroutine does not spin-waste CPU cycles. It sleeps at the OS level until a message arrives or the context fires.
Notice the ctx context.Context parameter. Go convention dictates that context always goes first and is named ctx. Functions that accept a context must respect ctx.Done() to allow graceful shutdown. The context package handles the heavy lifting of deadlines and cancellation propagation. When you call ctx.Cancel(), it closes an internal channel. The case <-ctx.Done(): branch unblocks instantly. Your loop runs one more time, hits the return, and the goroutine exits cleanly.
Context is plumbing. Run it through every long-lived call site.
Pitfalls and runtime behavior
An infinite loop is only dangerous when it has no escape hatch. The most common mistake is blocking on a channel receive without checking for cancellation. If the sender stops sending and never closes the channel, your goroutine sits in the loop forever. The runtime will not warn you. The goroutine just leaks memory and CPU scheduler slots until the process exits. Always pair for { } with select when you are waiting on channels. The select statement guarantees you can listen for a context cancellation signal alongside your data channel.
Another trap is the busy wait. Developers sometimes write for { if condition { break } } without any blocking operation inside. That loop consumes an entire CPU core and generates heat. The Go scheduler cannot preempt a goroutine that is running pure computation. It will starve other goroutines until the loop finishes or the process crashes. If you must poll, add a time.Sleep or use a time.Ticker inside a select block. The scheduler yields control during channel operations and timer waits, keeping your application responsive.
Error handling follows the same rule. If your loop calls a function that returns an error, check it immediately. The community accepts the if err != nil { return err } boilerplate because it makes the unhappy path visible. Swallowing errors inside an infinite loop creates silent failures that are nearly impossible to debug. Log the error, decide whether to retry or exit, and move on. The compiler will complain with err declared and not used if you assign an error and ignore it, which forces you to make an explicit choice about failure.
The worst goroutine bug is the one that never logs.
When to reach for an infinite loop
Use for { } when a process must run continuously until an external signal stops it.
Use a for loop with a condition when you know the exact number of iterations or have a boolean flag that changes state.
Use range over a channel when you want the loop to exit automatically once the channel closes.
Use a for loop with break when you need to validate data and stop early on the first error.
Use sequential code when the task finishes in a single pass: infinite loops belong in servers and workers, not in data transformations.
Infinite loops are engines. Give them fuel, and always wire the brake.