Loops
The idea
A computer's real advantage over doing something by hand is that it never gets tired of repeating a step. Picture a tireless assembly-line worker: give them one task and a stopping rule, and they repeat that task pass after pass, checking a counter each time, until it hits the limit you set. That worker is a loop. Go has exactly one looping keyword, for, and every kind of repetition in this lesson is built from it, just arranged differently depending on what you're counting.
The for loop forms
Think of for as the worker's instruction card. The card can carry up to three parts: where to start, the question that decides whether to keep going, and what changes after each pass. Depending on which parts you fill in, the same keyword does different jobs.
The three-component form fills in all three parts on one line:
for i := 0; i < 4; i++ {
fmt.Println("tick", i)
}
tick 0
tick 1
tick 2
tick 3
i := 0 runs once, before the first pass. i < 4 is checked before every pass; the loop stops the moment it's false. i++ runs after every pass. This is the form to reach for when you know exactly how many times to repeat something.
Leave out the start and the after-each-pass part, and you get the condition-only form — Go's version of what other languages call a while loop:
seconds := 3
for seconds > 0 {
fmt.Println(seconds)
seconds--
}
3
2
1
Only the keep-going question remains. You're responsible for changing seconds yourself inside the body, or the loop never stops.
Leave out all three parts and you get the infinite form, which repeats forever unless something inside it stops it:
size := 5
for {
size = size * 2
if size > 50 {
break
}
}
fmt.Println(size)
80
If a program like this ever runs forever by mistake, press Ctrl+C in the terminal to stop it. break is covered properly in a later section — for now, just notice that without it, this loop would never end. Go doesn't have a separate while or do-while keyword; the condition-only form already covers that case, and the infinite form covers "repeat until something inside says stop."
Range
Sometimes you don't want to manage a counter yourself — you just want to visit each position in turn. range does that visiting for you. As of Go 1.22, you can range over a plain number:
for i := range 3 {
fmt.Println("hello", i)
}
hello 0
hello 1
hello 2
That's the same job as a three-component loop, with no counter variable to declare or update yourself — range 3 hands you 0 through 2, one at a time.
range also works over a string, and this is where it earns its keep. It hands you two things at each step: the byte position the character starts at, and the character itself as a rune (one printed character, covered in an earlier lesson):
for i, r := range "né!" {
fmt.Printf("%d: %c\n", i, r)
}
0: n
1: é
3: !
%c prints a rune as the character it represents, rather than its number. Look at the positions: é takes two bytes, so the next character starts at position 3, not 2. A byte-by-byte walk could land in the middle of a multi-byte character, but range always decodes one whole character at a time and gives you the right starting position for each one.
Break and continue
Picture a foreman standing over the assembly line. break is the foreman shouting "stop the line" — the loop ends immediately, right where it is. continue is the foreman saying "skip this one, next" — the current pass ends early, but the loop keeps going.
for i := 1; i <= 10; i++ {
if i*i > 30 {
break
}
fmt.Println(i)
}
1
2
3
4
5
The loop never reaches 6 through 10 at all — break ends it the moment i*i goes past 30.
for i := 1; i <= 7; i++ {
if i%3 == 0 {
continue
}
fmt.Println(i)
}
1
2
4
5
7
Here the loop still runs all seven passes; continue just skips the fmt.Println for the multiples of three and moves straight to the next pass.
That % is the remainder operator from the variables lesson: it gives what is left over after a whole-number division. 7 % 3 is 1 because 3 fits into 7 twice with 1 left, and 10 % 5 is 0 because nothing is left. So i%3 == 0 asks "does 3 divide i exactly?".
Nested loops
A loop can sit inside another loop. Think of a warehouse inspector who checks every shelf, and on each shelf checks every item: for every single pass of the outer loop, the inner loop runs all the way through from its own start to its own end.
for outer := 1; outer <= 2; outer++ {
for inner := 1; inner <= 3; inner++ {
fmt.Println("outer", outer, "inner", inner)
}
}
outer 1 inner 1
outer 1 inner 2
outer 1 inner 3
outer 2 inner 1
outer 2 inner 2
outer 2 inner 3
The outer loop runs twice, and for each of those two passes, the inner loop runs all three of its own passes before control returns to the outer loop. That is 2 × 3 = 6 lines in total, and the output shows the order: the inner counter changes fastest.
The one gotcha worth knowing now: break and continue only affect the loop they're written directly inside. A break inside the inner loop stops the inner loop and lets the outer loop carry on to its next pass — it does not reach up and stop the outer loop too.
Accumulators and counters
A counter and an accumulator are both just a variable you declare before the loop starts, at a known starting value, and then update on every pass — like a running tally kept on a notepad, or a mechanical click-counter the inspector carries. A counter counts how many times something happened; an accumulator builds up a total or a result as you go.
balance := 100
for month := 1; month <= 3; month++ {
balance += 25
}
fmt.Println(balance)
175
balance += 25 is shorthand for balance = balance + 25. Declaring balance outside the loop, starting at 100, is what makes this work — if it were declared fresh inside the loop body, it would reset to 100 every single pass instead of building up.
spaces := 0
for _, r := range "go to the zoo" {
if r == ' ' {
spaces++
}
}
fmt.Println(spaces)
3
This counts the spaces in "go to the zoo". The _ in for _, r := range ... is the blank identifier — it tells Go "give me this value, but I'm deliberately not using it." Go normally refuses to compile a declared variable that's never used, so when range's index isn't needed, _ is the standard way to say so on purpose.
Common mistakes
- Forgetting to update the condition-only for loop's variable inside the body, so the loop never stops.
- An off-by-one border: using
<=when you meant<(or the reverse), running one pass too many or too few. - Expecting
breakinside a nested loop to stop both loops — it only exits the innermost one it's written in. - Treating
range 3's output as if it indexes into a collection — it's just the numbers0through2, nothing is being looked up. - Mixing up
%and/:/gives how many times a number fits (7 / 3is2),%gives what is left over (7 % 3is1). - Declaring an accumulator or counter inside the loop body instead of before it, which resets it to its starting value on every pass.
- Leaving out the
_when one ofrange's two values isn't needed, which Go rejects as an unused variable.
Try it
- Write the three-component, condition-only, and infinite for loops from this lesson, and run all three.
- Use
for i := range 3to print0through2, then range over the string"né!"and print each index and character with%d: %c. Notice the index jump afteré. - Write a loop from
1to10thatbreaks as soon asi*ipasses30. Then rewrite it tocontinuepast the multiples of3instead, so it prints1 2 4 5 7 8 10. - Print
7 % 3,10 % 5and2 % 5, and read the three answers. - Nest two for loops that print
outerandinneras in the lesson, then change the bounds to3and2and predict the number of lines before you run it. - Write a loop that adds
25to abalanceaccumulator three times, declared before the loop, and another that counts the spaces in"go to the zoo"usingrangeand_to ignore the index.