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) // 55

The 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) // 10

The 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) // 8

The 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) // true

It 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:

  • level is 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 new text.
  • abc is caught, but not because Go reported it. strconv.Atoi gave 0, as Chapter 15 warned, and 0 is outside the range. So the range does the work of catching bad text. If 0 were a valid answer, this check could not tell it from bad text. Chapter 21 shows how to read the error report from Atoi, 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 0 as the sentinel, any bad text would end the loop, since bad text gives 0 too.
  • continue from Chapter 18 skips the rest of the round, so a bad score is not added or counted.
  • count > 0 guards 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.67

Mixing 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.