t0ul

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.

← Back to the blog