In the last chapter, we split code across two packages (mathx and textx) and saw the if err != nil check for the first time. Now, we are going to make that pattern second nature. This chapter covers Go's control flow. You will learn how to use the for loop, write if statements with initialization steps, and use switch statements that break automatically.
We are adding a new file called control_flow.go with three demo functions. loopsDemo shows the standard for loop and for range. fizzBuzzDemo uses a condition-less switch to solve FizzBuzz. Finally, ifAndSwitchDemo combines an if statement with a multi-value switch to handle task statuses.

By the end of this chapter you will:
for loopif statements with initialization steps, including the if err != nil patternswitch statements without implicit fallthrough and with multiple values per caseCheck out the finish branch to follow along:
bashgit checkout 08-control-flow-finish
Go has exactly one loop keyword: for. It handles the job of for, while, and do/while found in other languages. You will not find a while keyword here.
The four forms look like this:
gofor i := 1; i <= 5; i++ { } // three-clausefor n < 100 { } // while-style (condition only)for { } // infinite loop (no condition)for i, v := range items { } // range over a collection
The three-clause form is the most explicit. The loopsDemo function uses it to count from 1 to 5:
go// control_flow.gofmt.Print("count up: ")for i := 1; i <= 5; i++ {fmt.Print(i, " ")}fmt.Println()
The i++ at the end runs after each iteration. Notice that there are no parentheses around the condition. Go does not allow them. If you add them by habit, your linter will catch the mistake.
The for range loop iterates over a collection like a slice. It gives you the index and the value on each iteration:
go// control_flow.gotasks := []string{"write code", "run tests", "ship it"}for i, title := range tasks {fmt.Printf("task %d: %s\n", i, title)}
If you only need the value, you can discard the index using the blank identifier _:
gofor _, title := range tasks { }
You can use for range on slices, maps, strings, and channels. We will rely on it constantly throughout the course.
A basic if statement looks the same as it does in most languages, just without the parentheses:
goif score > 90 {fmt.Println("excellent")}
Go also supports an init statement. This is a short statement that runs right before the condition. Any variables created here are scoped entirely to the if block:
goif init; condition {// init result is available here and in else}
This feature is incredibly useful when calling a function that returns two values. The if avg, err := ...; err != nil pattern is the standard way to handle errors in Go:
go// control_flow.goif avg, err := mathx.Average(8, 6, 10); err != nil {fmt.Println("average error:", err)} else {fmt.Printf("average score: %.1f\n", avg)}
Because avg and err are declared inside the if statement, they only exist for the if and else blocks. This keeps error variables tightly scoped so they do not clutter the rest of your function.
You saw mathx.Average in chapter 7. We are reusing it here inside control_flow.go to demonstrate the init pattern. To make this work, the mathx import moves from main.go into control_flow.go.
A switch statement in Go does not fall through to the next case by default. Once a case matches, the switch exits automatically. This prevents the common bugs found in C or JavaScript where a missing break causes unexpected behavior.
The most common form evaluates a specific value:
go// control_flow.goswitch status {case "todo", "in-progress":fmt.Printf("%-12s -> still open\n", status)case "done":fmt.Printf("%-12s -> finished\n", status)default:fmt.Printf("%-12s -> unknown\n", status)}
A single case can list multiple values separated by commas. If the status is "todo" or "in-progress", the first case matches. You do not need a break statement because Go handles the exit for you.
If you omit the value after the switch keyword, each case acts as a boolean expression. This provides a very clean alternative to a long if / else if / else chain:
go// control_flow.goswitch {case n%15 == 0:fmt.Println("FizzBuzz")case n%3 == 0:fmt.Println("Fizz")case n%5 == 0:fmt.Println("Buzz")default:fmt.Println(n)}
Go evaluates the cases from top to bottom. The first one that evaluates to true runs, and then the switch exits. This is why n%15 == 0 must come before n%3 == 0. If the number 15 passes both checks, we want to print "FizzBuzz", not "Fizz".
Here is the complete control_flow.go file on the finish branch:
go// control_flow.gopackage mainimport ("fmt""github.com/mt26691/go-for-beginners/mathx")func loopsDemo() {fmt.Println("\n-- Loops --")fmt.Print("count up: ")for i := 1; i <= 5; i++ {fmt.Print(i, " ")}fmt.Println()tasks := []string{"write code", "run tests", "ship it"}for i, title := range tasks {fmt.Printf("task %d: %s\n", i, title)}}func fizzBuzzDemo() {fmt.Println("\n-- FizzBuzz --")for n := 1; n <= 15; n++ {switch {case n%15 == 0:fmt.Println("FizzBuzz")case n%3 == 0:fmt.Println("Fizz")case n%5 == 0:fmt.Println("Buzz")default:fmt.Println(n)}}}func ifAndSwitchDemo() {fmt.Println("\n-- if & switch --")if avg, err := mathx.Average(8, 6, 10); err != nil {fmt.Println("average error:", err)} else {fmt.Printf("average score: %.1f\n", avg)}for _, status := range []string{"todo", "in-progress", "done", "archived"} {switch status {case "todo", "in-progress":fmt.Printf("%-12s -> still open\n", status)case "done":fmt.Printf("%-12s -> finished\n", status)default:fmt.Printf("%-12s -> unknown\n", status)}}}
The main.go file calls all three demo functions:
go// main.gopackage mainimport "fmt"func main() {fmt.Println("== Control Flow ==")loopsDemo()fizzBuzzDemo()ifAndSwitchDemo()}
Run the code:
bashmake run

The output matches exactly what the code produces. Notice the %-12s format verb in the status switch. This left-pads each label to 12 characters, which gives the -> arrows a clean column alignment in your terminal.
No parentheses around conditions. Writing if n > 0 { } is correct. Writing if (n > 0) { } will cause a compile error.
Braces are required. Even a single-line body needs { }. Go does not allow you to write an if or for statement without braces.
The fallthrough keyword exists but is rarely used. If a case absolutely must fall into the next one, you can write fallthrough explicitly at the end of the case block. You will not see this often.
break and continue work as expected. The break keyword exits the innermost for or switch. The continue keyword skips to the next iteration of the innermost for. If you have nested loops, you can use labels to break or continue an outer loop:
goouter:for i := 0; i < 3; i++ {for j := 0; j < 3; j++ {if j == 1 {break outer}}}
Labels are rarely needed, but they offer a clean solution when you are dealing with complex nested loops.
Type switches are a related concept. The syntax switch v := x.(type) matches on the concrete type behind an interface. This pattern shows up frequently in error handling and generic utilities. We will see it in context when we cover interfaces in chapter 11.
Chapter 9 covers structs, methods, and pointers. This is where you will model the Task resource that our API is built around. The if err != nil and for range patterns you practiced here will show up in almost every method we write moving forward.