Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.