Looping Constructs

Learn about loops in Go, the only looping keyword is for, used in multiple forms including classic for, condition-based while, infinite, and range-based iteration. Understand the essentials of the for statement, the range clause, and loop variable semantics.

Go loops allow you to execute the same block of code more than once without writing it multiple times. You’ll use loops for almost every task that involves collections, repeated calculations, reading data until a condition changes, or walking through a range of values. The language takes a deliberately minimal approach: there is exactly one keyword for looping, for. You shape it into different forms to cover every looping need you’ll encounter.

This page gives you a high‑level view of all the looping constructs Go provides. It introduces the three forms of the for statement, the range clause for iterating over data structures, and the loop variable semantics that can trip you up when capturing variables inside goroutines or closures. The full details, including edge cases and deeper examples, live in the dedicated sections that follow.

Why Go Has Only One Loop Keyword

Languages like C, Java, or JavaScript offer for, while, and do‑while. Each expresses a different looping intent, but they share the same underlying mechanism: check a condition, run a body, repeat. Go’s designers removed the need to choose among keywords. The for statement can be written in three syntactic shapes—a traditional initialisation‑condition‑post form, a condition‑only form that behaves like a while, and an empty form that loops forever. The compiler knows exactly what you mean, and you never need to remember which keyword goes with which loop style.

One keyword, many shapes:

Go’s single‑loop design does not limit what you can express. Every repetition pattern that other languages encode with while or do‑while has a clear for equivalent. This keeps the language simpler to learn and the code easier to refactor.

Beginners should think of for as a looping template. Depending on which parts you fill in, it becomes one of the familiar loop styles. Once you internalise the three shapes, you’ll never miss a separate while keyword.

The for Statement at a Glance

The for statement is the engine of every Go loop. Its full, classic shape looks like this:

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

The three parts inside the parentheses (which Go intentionally does not require) are executed in order: the init statement (i := 0) runs once before the loop begins; the condition (i < 5) is checked before each iteration—if false, the loop stops; the post statement (i++) runs after each iteration, just before the condition is re‑evaluated. This runs the body five times, printing 0 through 4.

If you drop the init and post statements, you get a loop that continues while the condition is true—exactly like a while loop in other languages:

n := 0
for n < 3 {
    fmt.Println(n)
    n++
}

Here, n is declared and initialised outside the loop, and the condition n < 3 controls repetition. The body must modify n, otherwise the loop would never stop. This shape is the natural choice when you don’t know ahead of time how many iterations you need.

Remove even the condition, and you have an infinite loop:

for {
    // do work, break somewhere
}

An infinite for runs until a break, return, or panic interrupts it. Servers, event loops, and long‑running workers often use this pattern. Without an explicit exit, the loop will block forever.

Infinite loops can freeze your program:

An infinite loop with no break or return inside will never yield control. Always ensure there is a termination path—whether through a conditional branch, a channel close, or a context cancellation. Forgetting to add an exit is one of the most common reasons a Go program stops responding.

When you run these examples, notice that the classic for and the condition‑only for both end cleanly after the specified number of iterations. The output is deterministic—the sequence of printed values is exactly what the condition and post statement imply. A loop that counts from 0 to 4 will always print 0, 1, 2, 3, 4, never 5. The condition is checked before the body, so the body never executes once the condition becomes false.

All three forms are just the for statement with some of its three parts omitted. Go’s grammar does not treat them as separate constructs; the compiler simply fills in the empty slots with no‑ops. This uniformity is why tools and formatters never have to decide whether a loop should be written with for or while—there is only one way.

The dedicated The for Statement section covers how to nest loops, how scope works inside the init statement, and subtle behaviours like the effect of continue on the post statement.

The range Clause

Iterating over the elements of a slice, map, string, or channel with an explicit index and length is verbose and error‑prone. The range clause turns this pattern into a single line that reads naturally:

fruits := []string{"apple", "banana", "cherry"}
for index, value := range fruits {
    fmt.Printf("element %d is %s\n", index, value)
}

Each time through the loop, range produces the current index (or key) and a copy of the element. You can also ignore the index or the value using the blank identifier _ if you only need one of them:

for _, fruit := range fruits {
    fmt.Println(fruit)
}

range works with arrays, slices, maps, strings (yielding runes and byte offsets), and channels. The behaviour adapts to the underlying type—for a map, iteration order is intentionally randomised; for a string, range decodes UTF‑8 and returns the Unicode code point, not a raw byte. These details are important when correctness depends on knowing exactly what range hands you.

Range is the idiomatic way to walk through collections:

Relying on range instead of a manual index loop reduces off‑by‑one mistakes and makes the intent visible at a glance. If your loop operates on every element of a slice, for _, v := range s is the clearest signal you can give to a reader.

Beginners often wonder why range returns a copy of the element rather than the element itself. Modifying the loop variable value does not change the original slice element. To mutate the slice in place, you need to index into it directly:

for i := range fruits {
    fruits[i] = strings.ToUpper(fruits[i])
}

This behaviour is a deliberate design choice that prevents accidental mutation of data you’re only supposed to inspect.

Loop Variable Semantics

One of the most surprising behaviours for newcomers involves capturing the loop variable inside a closure or a goroutine launched inside the loop. In older Go versions (before 1.22), the loop variable was reused across iterations. If you started a goroutine inside a for loop and referenced the loop variable, all goroutines would see the same final value. Consider this classic bug:

for _, v := range []int{1, 2, 3} {
    go func() {
        fmt.Println(v) // all print the same value (3, 3, 3 in pre‑1.22)
    }()
}

Each goroutine captures the variable v, not a snapshot of its value at the time the goroutine was created. By the time any goroutine actually runs, the loop has finished and v holds the last element.

Loop variable capture is a persistent source of concurrency bugs:

If you launch goroutines or defer a function that references the loop variable, double‑check that you’re capturing the intended value. The fix is to pass the variable as an argument to the closure, or rely on Go 1.22+ which changes the semantics so that each iteration gets its own variable.

Starting with Go 1.22, the loop variable is created anew for each iteration, aligning the behaviour with what most programmers intuitively expect. That change eliminates the need for the common workaround v := v, but the code you read in older codebases still uses that pattern, and you need to recognise it.

The mental model for beginners: a for loop is not a single block of time. The iterations happen sequentially, but closures and goroutines outlive the iteration that created them. Always ask yourself whether a value you capture inside a loop body will be the same when the closure finally executes. The simplest safe habit is to pass loop variables explicitly as parameters to any function that will run asynchronously.

Go 1.22 changed loop variable scoping:

Before Go 1.22, the loop variable was declared once and reused. Since Go 1.22, each iteration creates a new variable. Code you encounter may contain the v := v idiom to work around the old semantics—it’s harmless to keep it for backward compatibility, but new code targeting 1.22+ can safely omit it.

Understanding this nuance separates confident Go developers from those who end up debugging mysterious data races and stale values.