1. Why loops are needed, and why Go has only one
If you have ever tried to “just repeat an action 10 times” manually, you know how it usually ends: ten identical lines, and on the eleventh you made a mistake in a variable name. Loops are a way to tell the computer: “do this block of code several times, and let’s agree on when to stop.”
From a common-sense point of view, a loop is a control construct that repeats the body (the block { ... }) while some rule holds. In Go this is especially nice: there is only one loop — for. Yes-yes, there is no while as a separate keyword. Go is basically saying: “Why do you need two words if one is enough?”
for in Go: one construct for different tasks
It is important not to be scared of the phrase “the only loop.” It does not mean Go is “poor” — it means for has several forms. But today we are taking the most classic and the most useful for a beginner: a counter-controlled loop.
In real life this looks like “do it 5 times,” “go from 1 to N,” “print numbers from 0 to 9,” “read N values.” Almost all such tasks are conveniently expressed with the classic for, where you have a start, a condition, and a step.
2. Classic for: init; cond; post
General loop form
This form is like a classic sandwich: three layers with clear roles. It looks like this:
for init; cond; post {
// loop body
}
In Go you will see this form all the time — even in examples from the official materials, for example in an article about defer, where printing values is deferred inside a loop. That example uses the familiar counter i := 0; i < 4; i++, i.e., exactly the construct we are discussing right now.
Now let’s break it down (no stress, everything is logical).
init: preparation before the start
In the init section, you usually create and initialize the counter. And this is done once — before the first condition check.
Most often you write i := 0 or i := 1. Why sometimes zero and sometimes one is a matter of convenience and how you count in your head. The computer does not care; a person does. If you number “attempts 1..N,” you usually start with 1. If you work with indices (which we will later see in arrays/slices), you often start with 0.
Mini-example: declare a counter and get ready to print.
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
fmt.Println("Iteration", i) // Iteration 0 / 1 / 2
}
}
Notice a detail: i is declared with := right inside for. This is normal and very typical: the counter lives inside the loop and is almost never needed outside.
cond: the stopping condition
cond (condition) is a bool expression that is checked before each iteration. If the condition is true — the loop body runs. If it is false — the loop ends, and the program continues after the loop.
It is important to feel a simple idea: the condition is like a “guard at the entrance.” It does not let you into the next iteration if the rule is no longer satisfied.
Example for “print numbers 1..5” (inclusive):
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
fmt.Println(i) // 1 2 3 4 5
}
}
Here the condition i <= 5 means: “while i is not greater than 5 — keep working.”
Loop body: what exactly we repeat
The loop body is a regular block of code in braces. You have already seen such blocks in if/else and switch. In a loop, the block runs many times — and that is the only difference.
A good habit: keep the code inside the loop body as simple as possible. If there is too much going on inside the loop, it becomes hard to understand how many times things run and what exactly happens. While we are beginners, we write small, clear chunks, and leave “big walls of code” to people who enjoy suffering.
The simplest body example: print something a few times.
package main
import "fmt"
func main() {
for i := 0; i < 2; i++ {
fmt.Println("Go is fine") // Go is fine (2 times)
}
}
post: the step between iterations
post runs after the body on each iteration. This is the place where you usually change the counter: increase or decrease it so that the loop eventually ends.
If you forget to change the counter (or change it in the wrong direction), the loop can become infinite. But do not panic yet: this is a typical beginner mistake, and we will learn to catch it by sight.
Mini-example: step by 2.
package main
import "fmt"
func main() {
for i := 0; i < 6; i = i + 2 {
fmt.Println(i) // 0 2 4
}
}
Here post is i = i + 2. It runs after each fmt.Println.
i++ and i--: short form for “plus one” and “minus one”
In Go, counters have two very recognizable symbols from the world of “classic loops”: i++ and i--. This is a short form for “increase by 1” and “decrease by 1.”
An important reading tip: when you see i++, it almost always means “normal counter, normal iteration.”
Example with decrementing:
package main
import "fmt"
func main() {
for i := 3; i > 0; i-- {
fmt.Println(i) // 3 2 1
}
}
Here the condition i > 0 and the step i-- match: we go down toward zero, and the loop is guaranteed to stop.
Execution order: init → cond → body → post
Until you learn to run a loop in your head, any for will feel like magic. The good news: there is no magic — there is a strict order.
First, init runs (exactly once). Then cond is checked. If cond == true, the body runs. Then post runs. After that, the loop returns to checking cond. And so on, until the condition becomes false.
Here is a diagram that is useful to remember (like a “subway map”):
flowchart TD
A[init: once] --> B{cond?}
B -- true --> C[body: loop body]
C --> D[post: step]
D --> B
B -- false --> E[after the loop]
If you do not like diagrams, you can keep it as a table:
| Element | When it runs | Typical content |
|---|---|---|
|
once, at the start | |
|
before each iteration | |
|
if |
useful work |
|
after the body | |
3. Useful habits and important nuances
Habit: understand what values i will take
This is a super-skill for a beginner: being able to answer the question “what values will the counter take.” Not “approximately,” but specifically. Otherwise you will constantly end up in situations like “I expected 5 repeats, but got 6.”
Let’s break it down with an example:
for i := 0; i < 5; i++ { ... }
The counter starts at 0. Condition: i < 5. That means i will be: 0, 1, 2, 3, 4. Five will not be included, because when i == 5 the condition is false.
And here:
for i := 1; i <= 5; i++ { ... }
The counter: 1, 2, 3, 4, 5. Here 5 is included because the condition is “less than or equal.”
This seems like a small detail, but in practice it is half the battle with loops.
Why i is usually declared right in the for
When you write for i := 0; ..., you are basically saying: “the counter is a utility thing; let it live only here.” This helps avoid cluttering main with extra variables that later get accidentally used for the wrong purpose.
It also makes the code more “self-documenting”: you open the file, see the loop, and immediately understand where i came from and how it changes. No “who is i and why is it suddenly equal to 17” (although, of course, life still sometimes throws surprises).
init, cond, post are parts of one mechanism
A very common beginner mistake is to perceive init, cond, post as three formalities: like “everyone writes it this way.” In reality, these are three parts of a single control mechanism.
init is responsible for the initial state. cond is responsible for the rule “when do we continue.” post is responsible for moving the state toward completion. If any part is chosen poorly, the loop either runs the wrong number of iterations or “gets stuck” entirely.
A super-simple sanity check: try to answer two questions. Which variable changes on each iteration? And how does it move us closer to the moment when cond becomes false? If you cannot answer those questions, the loop is suspicious.
4. Example: TaskBox
So we are not learning for just for the sake of it, let’s start building a small application that will grow along with the course. Let’s call it TaskBox: for now, it is a very simple CLI that helps the user enter tasks and see them with numbering.
Today we do not have arrays and slices yet, so we still cannot “store a list of tasks.” But we can already do something useful: repeat input N times and nicely number what we entered. This already feels like a real app rather than “print hello 10 times.”
Read N and print task numbers
This sub-step helps you feel “how many times will the loop repeat” and what numbering looks like.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
fmt.Println("Task #", i) // for n=3: Task # 1 / 2 / 3
}
}
Here i is human-friendly numbering. We intentionally chose to start at 1 so the output looks friendly.
Repeat input N times and print what you entered
Now let’s add a “task name.” Reminder: fmt.Scan reads tokens, so for simplicity, let the task be one word (for example, buy_milk). That is fine for a learning step.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
var title string
fmt.Scan(&title)
fmt.Println(i, "-", title) // example: 1 - buy_milk
}
}
This is already a practical pattern: the loop controls the number of repeats, and inside we do the same operation (read and print).
5. One more example: a squares table
Sometimes it is useful to see that a loop is not only “input/output,” but also computations. Here we print a squares table from 1 to N. No new constructs — only for and multiplication.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
fmt.Println(i, "->", i*i) // for n=3: 1->1, 2->4, 3->9
}
}
This exercise (in the sense of “example”) is especially good because you can quickly visually verify the correctness of the result. If 16 suddenly appears where 3 should be — it means you “slipped” somewhere with the counter.
6. Typical mistakes when writing the classic for
Mistake #1: mixing up < and <= and getting an extra or missing iteration.
This is not “carelessness,” it really is the most common story. Your brain reads i < 5 and thinks “up to five,” but the computer reads it literally: “strictly less than five.” The cure is the habit of saying out loud: “the last value of i will be ...”.
Mistake #2: wrong counter start (for example, you started from 0, but you print “attempt #0”).
Technically the code is correct, but the user will look at “attempt #0” suspiciously, like a store receipt with “-1 item.” For human-friendly numbering, you more often start with 1. For indexing (when it appears later), you often start with 0 — but that is “later,” not “everywhere always.”
Mistake #3: the step does not match the condition.
If the condition is i < n, but the step is i--, then you will never get closer to i >= n. It is like driving toward the finish line, but after every turn you head in the opposite direction. The compiler is not obligated to save you from logic — it only enforces syntax.
Mistake #4: trying to “cram everything at once” into the loop header.
Sometimes beginners try to do it “cool”: complex initialization, tricky condition, a step with multiple variables — all at once. As a result, the loop becomes a mystery even to the author five minutes later. At the start, it is better to keep init simple (i := 0), cond clear (i < n), and post obvious (i++). Beauty will come when your brain stops spending effort on the basic mechanics.
GO TO FULL VERSION