3. Control Flow
Go has if, for, switch, and goto. That is the list. There is no while,
no do, no unless, no ternary ?:, and no comprehension. Chapter 1 showed
the shapes; this chapter fills them in and adds the parts that are actually
different from what you know.
Two rules hold everywhere. Conditions take no parentheses and braces are
never optional, not even around a one-line body. The second rule exists
because Go’s semicolon insertion requires the opening brace on the same line as
the if, and once the compiler is placing your braces there is nothing to argue
about.
3.1 If
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
The condition must be a bool. Nothing else is accepted, so if n and if s
and if p do not compile and must say n != 0, s != "", p != nil.
An if may run a short statement before its condition, with the names it
declares scoped to the if and its else:
if v, ok := m[key]; ok {
fmt.Println(v)
}
This is the shape you will write most often in real Go, because the two-value
returns of Chapter 4 land straight into it. Keeping v and ok out of the
surrounding scope is the point: they are answers to one question and they stop
existing when it is answered.
Since a function that fails returns the error as a value, Go code is full of this:
f, err := os.Open(path)
if err != nil {
return err
}
You will write that thousands of times. Chapter 8 is about what goes in the middle.
3.2 For
The three-clause form, the condition-only form, the bare infinite form, and
range, all introduced in section 1.4. Four details remain.
break and continue take a label, letting you leave an outer loop from
inside an inner one. A label is a name followed by a colon on the line before
the loop.
// A label lets break and continue name which loop they mean.
package main
import "fmt"
func main() {
const limit = 30
outer:
for n := 2; n <= limit; n++ {
for d := 2; d*d <= n; d++ {
if n%d == 0 {
continue outer
}
}
fmt.Print(n, " ")
}
fmt.Println()
}
2 3 5 7 11 13 17 19 23 29
continue outer abandons the current n and moves to the next one. Without the
label it would only abandon the current divisor, and every number would print.
Labels replace the flag variable you would otherwise introduce and check at the
top of the outer loop, and they are rare enough that seeing one is a signal to
read carefully.
range copies. for _, v := range xs gives you a copy of each element, so
assigning to v changes nothing. To modify the slice, index it: for i := range xs { xs[i] *= 2 }. The range expression itself is evaluated once, before the
loop, so appending to a slice inside a loop that ranges over it does not extend
the loop.
Each iteration gets a fresh loop variable.
// Each iteration gets its own copy of the loop variable.
package main
import "fmt"
func main() {
var fns []func()
for _, s := range []string{"a", "b", "c"} {
fns = append(fns, func() { fmt.Print(s, " ") })
}
for _, f := range fns {
f()
}
fmt.Println()
}
This prints a b c. Every closure captured a different s, because Go declares
a new one per iteration. Before Go 1.22 there was one variable reused across the
whole loop and the same program printed c c c, which was the single most
reported bug in the language. If you find older code that copies the loop
variable with s := s on the first line of the body, that is what it was
working around, and it is now harmless and unnecessary.
Ranging over a function is how Go spells a custom iterator, added in 1.23.
You will meet these as maps.Keys and slices.Values long before you write
one, and section 7.5 shows the consuming side.
3.3 Switch
Go’s switch is the one place where the language is more generous than C rather
than less.
// Switch on a value, with several matches per case.
package main
import "fmt"
func classify(r rune) string {
switch r {
case 'a', 'e', 'i', 'o', 'u':
return "vowel"
case ' ', '\t', '\n':
return "space"
}
switch {
case r >= '0' && r <= '9':
return "digit"
case r >= 'a' && r <= 'z':
return "consonant"
}
return "other"
}
func main() {
for _, r := range "go 1.23!" {
fmt.Printf("%q %s\n", r, classify(r))
}
}
'g' consonant
'o' vowel
' ' space
'1' digit
'.' other
'2' digit
'3' digit
'!' other
Cases do not fall through. The body of a matching case runs and the switch
ends, with no break needed, which removes the most common C bug in the
construct. When you want fall-through, the keyword fallthrough as the last
statement in a case does it, and you will use it about once a year.
A case can list several values, separated by commas, as the vowels do above.
Cases need not be constants. They are evaluated in order and compared
against the switch value, so case r >= '0' is legal in a switch on r.
The value is optional. A switch with nothing after the keyword tests
true, so each case is an independent condition, and the construct becomes a
tidier if-else chain:
// A switch with no value tested acts as an if-else chain.
package main
import "fmt"
func main() {
for i := 1; i <= 15; i++ {
switch {
case i%15 == 0:
fmt.Println("FizzBuzz")
case i%3 == 0:
fmt.Println("Fizz")
case i%5 == 0:
fmt.Println("Buzz")
default:
fmt.Println(i)
}
}
}
Prefer that form over if/else if whenever there are more than two branches.
It lines the conditions up in a column where a reader can see them all at once,
and adding a branch does not touch the others.
A switch also accepts a short statement first, like if: switch x := f(); {.
And there is a second kind, the type switch, which asks what concrete type is
inside an interface value; it needs Chapter 9 and appears there.
3.4 Goto
Go has goto and you will not use it. It jumps to a label in the same function
and may not jump over a variable declaration or into a block. Generated code and
the occasional deeply nested parser loop are where it actually appears.
The one thing you might reach for it for, unwinding after a failure partway
through a function, has a better answer in defer, section 4.4.
Exercise 3-1. Rewrite classify so that the whole thing is one tagless
switch. Decide which version you would rather read in six months.
Exercise 3-2. Write a program that reads lines from standard input and prints only those that are palindromes, ignoring case and spaces.
Exercise 3-3. The primes program tests divisors up to the square root.
Add a counter for how many divisions it performs, and compare it against a
version that tests every divisor up to n-1.