Automate the Boring Stuff with Go, Ch. 3: Control Flow
Part of Automate the Boring Stuff with Go, a beginner-friendly guide to programming in Go.
Updated for Go 1.27 in October 2026. What changed: math/rand/v2 (no more seeding with the clock), for i := range n, a note on loop variables, and the first example no longer calls every number odd.
In the previous chapter we covered the basics of Go data types and, in a few examples, used flow control to print messages.
In this chapter we’ll use what we learned about comparison operators and boolean values to learn more about flow control. Every example is in the repo under ch3.
Elements of Flow control
Programs are like water in a river: they tend to flow in one direction. And like water in a river, parts can go off into separate outlets depending on conditions, a boulder in the stream or a dam.
Unlike water, it is easy to divert the flow of a program. We use conditions to evaluate a variable’s value or the output of a function, and run a different block of code depending on the result.
Here is the familiar if from the previous chapter, with comments:
// Ex. 1 Tell me if a number is even or odd
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
// pick a random whole number from 0 to 99
n := rand.IntN(100)
// 'if' condition
if n%2 == 0 {
// code block that runs only when the condition is true
fmt.Printf("Number is even: %d\n", n)
return // leave main, so the line below does not run
}
// reached only when the condition was false
fmt.Printf("Number is odd: %d\n", n)
}
This program checks whether a random whole number is even using the modulo operator %, which is a fancy way of saying “divide and keep the remainder”. An even number divides by two with no remainder; an odd number does not.
Don’t worry about the math. Note that the program’s path changes based on the condition, which depends on the random value of n. The return inside the if is what keeps the odd message from printing too: it ends main early.
The random number comes from math/rand/v2. Older Go code (and the first edition of this chapter) seeded the generator from the clock first; you no longer need to. It is seeded for you.
Now, let’s introduce the rest of the gang.
Flow Control Statements
if
The most used control statement. The pattern is a block of code that runs only if the condition is true.
else
In the first example we could have used an else to print the odd message:
// ex. 2 else
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
n := rand.IntN(100)
if n%2 == 0 {
// runs when the condition is true
fmt.Printf("Number is even: %d\n", n)
} else {
// runs when it is not
fmt.Printf("Number is odd: %d\n", n)
}
}
else says “if the condition turns out false, do this instead”. Many Go programmers prefer the first form, an early return and no else, because the happy path stays at the left margin. Both are correct; pick the one that reads better in the moment.
else if
Here we can say what should happen when the first condition is false and a second one is true:
// ex. 3 else if
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
n := rand.IntN(100)
if n%2 == 0 {
fmt.Printf("Number is even: %d\n", n)
} else if n%2 == 1 {
fmt.Printf("Number is odd: %d\n", n)
}
}
switch
switch does the same job as a chain of if/else if, but reads as a list of cases. A case is a condition. Those with a mathematical background may prefer it; either style works.
// ex. 4 switch
package main
import (
"fmt"
"time"
)
func main() {
// `switch` without an expression is an alternate way
// to express if/else logic. Here we also show how the
// `case` expressions can be non-constants.
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
}
In the first switch we hand it a value and the cases are compared against it. In the second there is no value, so each case is its own condition, which is just if/else if in different clothes:
// ex. 5 convert switch to if else
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
if t.Hour() < 12 {
fmt.Println("before noon")
} else if t.Hour() > 12 {
fmt.Println("it's after noon")
}
}
I prefer switch with the optional default case because it is easier to see every path at a glance and to tell a reader what I meant.
Loops
Now that we can steer a program, it’s time for loops. A loop tells the computer “do this work until a condition says stop”. For example, the saying “a thousand thanks”: here is a program that thanks a reader a thousand times by name.
// ex 6. If you're a reader, then a thousand thanks
package main
import "fmt"
func main() {
fmt.Println("Enter your name")
var n string
fmt.Scan(&n)
switch {
case n == "Toul":
fmt.Printf("The author is %s\n", n)
default:
// range over an integer counts 0, 1, 2 ... 999
for i := range 1000 {
fmt.Printf("i: %d Thank you %s for reading\n", i, n)
}
}
}
for i := range 1000 is the short way to count from 0 to 999. The long way, which you will see everywhere, is for i := 0; i < 1000; i++ {, read as start here; keep going while this is true; do this after each round. Both forms give you i fresh on every round, so if you ever hand i to something that runs later (chapter 14 does), it keeps the value it had on that round.
In most programming languages it is normal to count from 0, so keep that in mind when working with slices and loop counters.
Okay, let’s have some fun building two small games to show friends and family at the next holiday party.
Don’t worry if some syntax is unfamiliar. We want muscle memory and familiarity with Go, so try to type the code in and get it running. If you have trouble, the source code is in the repo.
Guess the Number
The computer picks a random number and you try to guess it, but you only get three attempts. To make it winnable, the program tells you whether to go bigger or smaller.
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
fmt.Println("Game: Guess a number between 0 and 9")
fmt.Println("You have three (3) tries")
// math/rand/v2 is seeded for you; no clock tricks needed
secretNumber := rand.IntN(10)
var guess int
for try := 1; try <= 3; try++ {
fmt.Printf("Attempt %d. Enter your number: ", try)
if _, err := fmt.Scan(&guess); err != nil {
fmt.Println("I don't recognize that input. Try a number like '1'")
continue
}
switch {
case guess == secretNumber:
fmt.Println("You win!")
return
case guess < secretNumber:
fmt.Println("Sorry, wrong guess; the number is bigger")
case guess > secretNumber:
fmt.Println("Sorry, wrong guess; the number is smaller")
}
}
fmt.Printf("Game over! The correct number was %d\n", secretNumber)
}
Two things to notice. fmt.Scan returns an error when the input is not a number, and we use it: a letter does not count as one of your three tries. And when you win, return ends the game instead of asking for more guesses.
Advanced: Rock, Paper, Scissors
Now for a bigger example to whet your appetite.
package main
import (
"fmt"
"math/rand/v2"
"strings"
)
// Move is one of the three choices. Giving it a named type keeps the rest of the
// program from mixing it up with any other int.
type Move int
const (
Rock Move = iota
Paper
Scissors
)
var names = [...]string{Rock: "Rock", Paper: "Paper", Scissors: "Scissors"}
var validInput = map[string]Move{
"r": Rock, "rock": Rock,
"p": Paper, "paper": Paper,
"s": Scissors, "scissors": Scissors,
}
// outcome is indexed by (computer - player + 3) % 3.
var outcome = [...]string{"You tied.", "You lost.", "You win!"}
func getInput() string {
fmt.Print("Pick [r]ock, [p]aper, or [s]cissors (ctrl+c to quit): ")
var input string
fmt.Scanln(&input)
// lower-casing input is a good habit: users type however they like
return strings.ToLower(input)
}
func playerMove() Move {
// loop until the input is one we understand
for {
if move, ok := validInput[getInput()]; ok {
fmt.Println("You chose", names[move])
return move
}
fmt.Println("I didn't understand that, please retry")
}
}
func main() {
// an endless loop: the game runs until the player presses ctrl+c
for {
player := playerMove()
computer := Move(rand.IntN(3))
fmt.Printf("Computer chose %s\n", names[computer])
fmt.Printf("Result: %s\n\n", outcome[(computer-player+3)%3])
}
}
The for { with no condition in main is an endless loop: the game runs until you press ctrl+c, which is how every terminal program is told to stop. Other languages call this a while loop, and it is how video games work: while the player is playing, keep running the game.
The rest is the chapter so far put together: a map to translate what the user typed into a move, a loop that repeats until the input is valid, and a bit of arithmetic that turns “computer minus player” into win, lose or tie.