AlgoMaster Logo

For Loop

Last Updated: May 22, 2026

High Priority
10 min read

Go has exactly one loop keyword: for. There's no while, no do-while, no foreach. One construct covers every looping case, from counting through a list of products to waiting for a stock check to succeed. This lesson covers the three forms of for and the patterns that show up over and over in real Go code.

The Three Forms of for

Every for loop in Go is one of three shapes:

FormLooks LikeUse For
Three-clausefor init; cond; post { }Counting a known number of times
Condition-onlyfor cond { }Looping while something is true (the "while" form)
Infinitefor { }Looping until you explicitly stop

A fourth form, for k, v := range x, deserves its own lesson and is covered next. This lesson focuses on the three forms above.

The three shapes look different on the surface, but they're the same statement underneath. The condition-only form is just the three-clause form with the init and post parts missing. The infinite form is the three-clause form with all three parts missing. Once you see that, the rest is straightforward.

The diagram shows the full lifecycle of a three-clause loop. init runs once at the start. Then cond is checked. If it's true, the body runs and post runs after. Control jumps back to the condition check. The moment cond is false, control falls through to whatever comes after the loop. The condition-only and infinite forms follow the same path, they just skip the init step or the cond check (or both).

Three-Clause Form

The three-clause form is the one you'll use most often when you know how many iterations you want.

Three things: there are no parentheses around the three clauses (putting them there is a compile error), the braces are required even for a single-statement body, and i is declared in the init clause and exists only inside the loop. After the loop ends, i is no longer in scope.

A common E-Commerce use is printing a numbered list of products:

The condition i < len(products) is checked before each iteration. When i reaches 4, the condition is false and the loop exits. The range form is more idiomatic for this exact case, but the three-clause form is appropriate when you also need the index for arithmetic, when you want to skip elements, or when you're iterating something that isn't a collection.

Post-Statement Variations

The post clause doesn't have to be i++. Anything that's a valid statement works. A few patterns that come up in practice:

Counting down is useful when you're processing items from the end of a list, for example showing the most recent orders first. Stepping by a fixed amount comes up in pagination, where you process records in batches. Multiplying by a factor is the pattern behind exponential backoff, which is used in retries.

You can also have multiple operations in the post clause using a comma expression, but Go doesn't allow the C-style i++, j++ syntax directly. Instead, you use parallel assignment:

The init clause declares both head and tail. The post clause uses parallel assignment to update both at once. This is the closest Go gets to C's comma operator, and it's clean once you see the pattern.

While-Style: for cond { }

Go has no while keyword. When you want a loop that runs as long as some condition is true, you write for cond { }. It's the same for, just without the init and post clauses.

The condition stock > 0 is checked at the top of each iteration. If you're coming from a language with while, this is the same idea with a different keyword. The body has to do whatever changes the condition (here, decrementing stock), otherwise you have an infinite loop on your hands.

A more realistic E-Commerce example: draining a queue of pending orders until none are left.

Each iteration takes the first order, removes it from the slice (pendingOrders[1:] gives a new slice without the first element), and continues until the slice is empty. The condition is the natural stopping point: when there's nothing left, you're done.

Infinite Loop: for { }

When you don't have a clear stopping condition at the top of the loop, you can write for { } with no clauses at all. This is an infinite loop, and you exit it from inside the body using return or break.

break exits the innermost enclosing loop. The short version is that break jumps to the statement immediately after the loop, and continue skips to the next iteration.

return is the other way out. If you're inside a function and you want to exit both the loop and the function at the same time, return does it in one step:

This function walks the stock list looking for the first product that has any in stock. The infinite loop is a clean way to write it because there are two distinct ways to exit (no items left, or we found one) and each one wants its own return value.

The infinite form shows up in server loops, retry loops, and any place where the stopping condition is something other than a counter or a fixed predicate. The loop body owns the exit, not the loop header.

Why Go Has Only for

Most popular languages have at least three loop keywords: while, do-while, and for (or foreach). Go has one. This isn't an oversight, it's a deliberate language design choice with a few reasons behind it.

Fewer keywords means a smaller language. Go's spec is famously short, and one of the ways the designers kept it short was by avoiding redundant constructs. A while loop is already expressible as for cond { }. A do-while is expressible as for { ... if !cond { break } }. Having extra keywords would add to what you have to learn without adding any new capability.

Consistency is easier to read. Every loop in a Go codebase starts with for. You don't have to remember whether the team prefers while or a counted for, or whether a do-while is even idiomatic. There's exactly one way to write a loop, and the form tells you what it does.

The init and post clauses give the three-clause form a scope advantage. A variable declared in for i := 0; ... exists only inside the loop. In a language with both while and for, you'd often declare the counter outside the while, polluting the surrounding scope. The three-clause for keeps that variable local.

The trade-off is that Go doesn't have do-while directly, where the body runs once before the condition is checked. If you need that behavior, you write it explicitly:

The body runs once unconditionally, then the if check at the bottom decides whether to keep going. It's slightly more code than do { } while (cond) would be, but it's also explicit about what's happening: the loop is infinite, and the body owns the exit.

Nested Loops

A loop body can contain another loop. The inner loop runs to completion for each iteration of the outer loop. This is the natural way to walk a grid, compare every pair of items, or process a list of lists.

A small E-Commerce example: printing every combination of size and color for a product.

The outer loop picks a size, then the inner loop runs all the way through the colors before the outer loop moves on. Total iterations is len(sizes) * len(colors), which is 3 * 2 = 6 here.

Nested loops scale quickly. Two levels of 100 elements each is 10,000 iterations. Three levels of 100 is a million. Watch for accidentally quadratic patterns, especially when comparing every pair of items in a list.

Another common nested pattern is walking a 2D grid. A stock table where rows are products and columns are warehouses:

The outer loop walks rows, the inner loop walks columns. Pay attention to len(stock[row]) rather than hard-coding 3: it makes the code work for non-rectangular grids and survives later edits to the data shape.

Nested loops can be exited in several ways. A plain break only exits the innermost loop. To break out of an outer loop from inside an inner one, you need labeled break statements.

Common Patterns

A handful of for loop patterns show up so often that they're worth knowing by sight.

Counting

A counter starts at its zero value and increases by one each pass. The classic accumulator.

This pattern is the foundation of every "how many of X" calculation. The variable starts at its zero value (0 for int), the loop increments it conditionally, and the final value is the count.

Accumulating a Total

The same shape as counting, but the increment is variable rather than 1.

total starts at 0.0. Each iteration adds one price to it. After the loop, total holds the sum. This is one of the most common loops you'll write in any Go codebase that deals with quantities or money.

Retrying with a Counter

When you want to try something a limited number of times before giving up, a three-clause loop is the cleanest way to write it.

The loop puts an upper bound on how many times you try. The counter is also useful inside the body, you could pass it as a delay multiplier for exponential backoff, or include it in a log message. If the function succeeds, return exits both the loop and main. If it never succeeds, the loop ends naturally and the fallback line runs.

This pattern is the structure behind almost every retry loop in production Go code. The number of attempts is bounded, the success case exits early, the failure case is handled at the end.

Waiting for a Condition

Sometimes you don't know how long something will take, you just need to keep checking until it's true.

This is the condition-only form pulling its weight. There's no counter, no fixed iteration count, the loop just keeps going until the condition becomes false. In real code you'd add a sleep between checks and a maximum number of checks so you don't burn CPU or wait forever, but the shape of the loop stays the same.

Comparing the Forms Side by Side

The three forms differ mostly in which clauses are present. Here are the same five iterations written in each shape.

All three produce identical output. The differences are in where the counter lives and where the loop logic lives.

FormCounter ScopeWhere Exit Logic LivesWhen to Use
Three-clauseInside the loopIn the headerFixed iteration count
Condition-onlyOutside the loopIn the headerLoop while a condition holds
InfiniteN/A or outsideIn the bodyMultiple exit paths or unknown count

For a fixed-count loop, the three-clause form is usually the cleanest because it puts everything (initialization, condition, step) in one line and keeps the counter local. For a loop driven by external state (a queue, a flag, a remote service), the condition-only form or the infinite form makes more sense because the exit isn't a simple counter.

Empty Body and Empty Clauses

A for loop body can be empty if the work is all in the header. This is rare in Go but legal:

Notice the leading semicolon before the condition. Even with the init clause empty, the semicolons are still there to mark the three slots. The body has no statements. Most of the time, an empty body means you should rewrite the loop, but it can be useful when you just want a counter at the end.

You can also leave the init or the post clause empty in any combination:

These hybrid forms are unusual in practice. If you find yourself using them, one of the cleaner three forms is almost always a better fit.

A Larger Example: Building an Order Summary

Putting several patterns together, here's a small program that processes a list of orders, prints each one, computes a running total, and bails out early if the total exceeds a daily cap.

Three things from earlier sections show up here. A three-clause loop walks the orders by index. A running total accumulates into a float64 that started at its zero value. An early exit happens when a condition inside the body becomes true, in this case via break. The final summary uses both processed (the counter) and total to report what was done, which is what makes the early exit graceful: the program doesn't pretend it finished, it tells you exactly what got through.