20. Loop patterns
Almost every loop is one of a few shapes. Name them and you stop inventing each loop from scratch, and reach for the one that fits. This chapter covers six, each doing one small job. Accumulate, count, search, and flag use a range of numbers here, and the shapes work on anything you can loop over. Input validation and the sentinel read from the keyboard.
1. Accumulate: build up a result
Hold a variable and change it every round. The running total is the classic case:
sum := 0
for i := 1; i <= 10; i++ {
sum += i
}
fmt.Println(sum) // 55The shape is always: set a start value before the loop,
update it inside, use it after. It also builds strings
(start "", join on each piece with +=) and
finds a maximum (start low, raise it whenever you see something
bigger).
2. Count: how many match?
A counter is an accumulator that goes up by one, but only when a test passes. "How many numbers from 1 to 30 divide evenly by 3?"
hits := 0
for i := 1; i <= 30; i++ {
if i%3 == 0 {
hits++
}
}
fmt.Println(hits) // 10The if inside the loop is the test; hits++
is the tally.
3. Search: find the first match and stop
When you only need the first value that fits,
break out the moment you find it. There is no point
continuing:
first := -1
for i := 1; i <= 100; i++ {
if i*i > 50 {
first = i
break
}
}
fmt.Println(first) // 8The answer is 8, because 8 × 8 = 64 is the first square over 50.
first needs a starting value, and a Go int always holds
a number. So pick a number that can never be a real answer.
-1 works here because the search starts at 1, and it is the
same "not found" answer strings.Index gives in Chapter 14.
If nothing matched, first stays -1, and you
can test that with if first == -1.
4. Flag: did any round match?
Sometimes you want a yes or no, not the value: "is any number from 1
to 20 divisible by 7?" Keep a bool that starts false and
flips to true the moment you see a match:
found := false
for i := 1; i <= 20; i++ {
if i%7 == 0 {
found = true
}
}
fmt.Println(found) // trueIt prints true because 7 and 14 are in range. If you
only care whether one exists, break when the flag
flips, which turns it into a search. Flag and search are cousins.
Open exercises/20/01-patterns.go. It accumulates the sum
of 1 to 20. Add the count pattern below it to print how many numbers in
that range are even.
5. Input validation: ask until the answer is good
Chapter 18 showed that a bare for with
break runs at least once. That fits a question: ask, check,
and ask again if the answer is no good. This is
exercises/20/02-validate.go, inside main:
reader := bufio.NewReader(os.Stdin)
var level int
for {
fmt.Print("Level (1 to 99): ")
text, _ := reader.ReadString('\n')
level, _ = strconv.Atoi(strings.TrimSpace(text))
if level >= 1 && level <= 99 {
break
}
fmt.Println("That is not a level from 1 to 99.")
}
fmt.Println("Level set to", level)A session:
Level (1 to 99): abc
That is not a level from 1 to 99.
Level (1 to 99): 150
That is not a level from 1 to 99.
Level (1 to 99): 12
Level set to 12
Three details here:
levelis declared before the loop, so that it still exists after the loop. Inside,level, _ =uses plain=to change it.text, _ :=is fine inside the loop, even though it runs many times. Each round is a fresh run of the block, so each one declares a newtext.abcis caught, but not because Go reported it.strconv.Atoigave0, as Chapter 15 warned, and0is outside the range. So the range does the work of catching bad text. If0were a valid answer, this check could not tell it from bad text. Chapter 21 shows how to read the error report fromAtoi, and that fixes it.
A validation loop only ends with a good answer. If the input runs
out, for example when it comes from a file and not the keyboard, every
later read gives "", the number is 0, and the
loop prints its message forever. Stop it with Ctrl + C.
Reading the second value from ReadString, which Chapter 21
explains, is how a program notices this.
6. Sentinel: read until a special value
Often you do not know how many values are coming. A
sentinel is a special input that means "that is all".
This program reads scores until the word done, then prints
their average. This is exercises/20/03-sentinel.go, inside
main:
reader := bufio.NewReader(os.Stdin)
total := 0
count := 0
for {
fmt.Print("Score from 1 to 100, or done: ")
text, _ := reader.ReadString('\n')
answer := strings.TrimSpace(text)
if answer == "done" {
break
}
score, _ := strconv.Atoi(answer)
if score < 1 || score > 100 {
fmt.Println("Skipped.")
continue
}
total += score
count++
}
if count > 0 {
average := float64(total) / float64(count)
fmt.Printf("%d scores, average %.2f\n", count, average)
} else {
fmt.Println("No scores entered.")
}A session:
Score from 1 to 100, or done: 80
Score from 1 to 100, or done: abc
Skipped.
Score from 1 to 100, or done: 91
Score from 1 to 100, or done: 150
Skipped.
Score from 1 to 100, or done: 74
Score from 1 to 100, or done: done
3 scores, average 81.67
This one loop uses patterns you already know: it accumulates
total and counts with count. Three more
details:
- The sentinel is checked before the conversion,
because it is text. It also avoids a trap: with a number such as
0as the sentinel, any bad text would end the loop, since bad text gives0too. continuefrom Chapter 18 skips the rest of the round, so a bad score is not added or counted.count > 0guards the division, as in Chapter 17.
The average is where Go's types matter. total and
count are ints, so total / count is whole
number division and drops the fraction. Convert both to float64 first,
as in Chapter 13:
total := 245
count := 3
fmt.Println(total / count) // 81
fmt.Printf("%.2f\n", float64(total)/float64(count)) // 81.67Mixing the patterns
Real programs combine these. One pass can accumulate a total, count how many fit a rule, and flag whether something special appeared, all at once:
total := 0
evens := 0
hasMultipleOf5 := false
for i := 1; i <= 10; i++ {
total += i // accumulate
if i%2 == 0 {
evens++ // count
}
if i%5 == 0 {
hasMultipleOf5 = true // flag
}
}
fmt.Println("total", total)
fmt.Println("evens", evens)
fmt.Println("multiple of 5?", hasMultipleOf5)Output:
total 55
evens 5
multiple of 5? true
One loop, three patterns, three answers.
Homework
Homework starters are in exercises/20/homework/. Each
one runs as it is and prints a placeholder line.
Problem 1 — Factorial
Open exercises/20/homework/01-product.go. Using the
accumulate pattern, multiply the numbers 1 to 5 together and print the
result. (Hint: a product starts at 1, not 0,
since anything times zero is zero.)
Problem 2 — Count multiples
Open exercises/20/homework/02-count-multiples.go. Using
the count pattern, print how many numbers from 1 to 50 divide evenly by
7.
Problem 3 — First big square
Open exercises/20/homework/03-first-big.go. Use the
search pattern to find and print the first number whose square is over
200, checking from 1 upward and stopping with break.
Challenge — Range stats in one pass
Open exercises/20/homework/04-stats.go. In one loop over
1 to 20, work out and print three things: the total (accumulate), how
many are even (count), and whether any number is divisible by 9
(flag).
Stuck or finished? Open the homework solutions page.