Free Handbook · Every example compiled & verified

Flow Control

if with an init statement, for as Go's only loop in all its forms, switch without fallthrough, labelled break and continue, and defer.

0 / 134 lessons🔥 0 day streak
ShareXLinkedIn

Module 02 · what you'll be able to do

  • Write if statements with an init statement that scopes a variable to the if/else chain
  • Use for as a counting loop, a while loop, an infinite loop and a range loop
  • Replace long if/else chains with a switch, including the condition-less switch
  • Break out of nested loops with a label, and know why a bare break inside a switch does not leave the loop
  • Predict when deferred calls run and in what order
01

if, else and the init statement

Go's if has no parentheses around the condition, and the braces are always required — even for one line. The condition must be a real bool: Go has no "truthy" values, so if count does not compile when count is an int.

An if can start with a short init statement before the condition, separated by a semicolon: if v := compute(); v > 10 { ... }. The variable exists only inside the if and its else branches. This is the idiomatic way to check an error without leaking err into the rest of the function.

gomain.go
package main

import (
	"fmt"
	"strconv"
)

func grade(score int) string {
	if score >= 90 {
		return "A"
	} else if score >= 75 {
		return "B"
	} else {
		return "C"
	}
}

func main() {
	fmt.Println(grade(95), grade(80), grade(40))

	for _, text := range []string{"42", "forty"} {
		if n, err := strconv.Atoi(text); err != nil {
			fmt.Println("not a number:", text)
		} else {
			fmt.Println("doubled:", n*2)
		}
		// n and err do not exist here
	}
}
Outputcompiled & run with real Go
A B C
doubled: 84
not a number: forty
Your turn

Add a "A+" grade for scores of 98 and above. Where must that branch go in the chain, and why?

Return early instead of nesting
Idiomatic Go handles the error or edge case first and returns, so the "happy path" stays at the left margin: if err != nil { return err }. Deeply nested else blocks are a code-review smell. Linters even flag an else after a branch that ends in return.
Error you will hit

non-boolean condition in if statement

go
package main

import "fmt"

func main() {
	count := 3
	if count {
		fmt.Println("have items")
	}
}
# command-line-arguments
./main.go:7:5: non-boolean condition in if statement
Why the compiler said that

In C, JavaScript or Python a non-zero number counts as true. Go deliberately has no truthiness: a condition must have type bool, so the intent is always written out.

The fix

Write the comparison you mean.

go
package main

import "fmt"

func main() {
	count := 3
	if count > 0 {
		fmt.Println("have items")
	}
}
Error you will hit

There is no ternary operator

go
package main

import "fmt"

func main() {
	age := 20
	label := age >= 18 ? "adult" : "minor"
	fmt.Println(label)
}
# command-line-arguments
./main.go:7:21: invalid character U+003F '?'
./main.go:7:23: syntax error: unexpected literal "adult" at end of statement
Why the compiler said that

Go left out ? : on purpose — the designers found nested ternaries hard to read. ? is not even a valid character in Go source outside strings.

The fix

Declare the variable, then set it with an if.

go
package main

import "fmt"

func main() {
	age := 20
	label := "minor"
	if age >= 18 {
		label = "adult"
	}
	fmt.Println(label)
}
02

for: the only loop

Go has exactly one loop keyword, for, and it covers every case. The classic three-part form (init; condition; post) counts. Drop the init and post and you have a while loop. Drop everything and you have an infinite loop that you leave with break or return. And for ... range walks over slices, strings, maps, channels — and, since Go 1.22, plain integers.

gomain.go
package main

import "fmt"

func main() {
	// 1. classic three-part loop
	for i := 0; i < 3; i++ {
		fmt.Print(i, " ")
	}
	fmt.Println()

	// 2. "while" loop: condition only
	n := 1
	for n < 100 {
		n *= 3
	}
	fmt.Println("n =", n)

	// 3. infinite loop with break
	tries := 0
	for {
		tries++
		if tries == 4 {
			break
		}
	}
	fmt.Println("tries =", tries)

	// 4. range over an integer (Go 1.22+)
	for i := range 3 {
		fmt.Print(i*10, " ")
	}
	fmt.Println()

	// 5. range over a slice: index and value
	for i, lang := range []string{"go", "rust"} {
		fmt.Println(i, lang)
	}
}
Outputcompiled & run with real Go
0 1 2 
n = 243
tries = 4
0 10 20 
0 go
1 rust
Your turn

Change loop 2 so it counts how many times it multiplied. Print that count too.

VisualizeStepping through a three-part for loopStep 1 / 9
sum := 0
for i := 1; i <= 3; i++ {
sum += i
}
fmt.Println(sum)
Line 1

sum starts at 0.

Variables now
sum0
All 9 steps as a table
StepLineWhat happenedVariables now
11sum starts at 0.sum = 0
22The init statement runs once: i := 1. The condition 1 <= 3 is true, so the body runs.sum = 0 i = 1
33sum += isum = 1 i = 1
42The post statement i++ runs, then the condition is checked again: 2 <= 3.sum = 1 i = 2
53sum += isum = 3 i = 2
62i++ makes 3; 3 <= 3 is still true.sum = 3 i = 3
73sum += isum = 6 i = 3
82i++ makes 4; 4 <= 3 is false, so the loop ends. i goes out of scope.sum = 6
95Print the total.sum = 6
gomain.go
package main

import "fmt"

func main() {
	// range over a string yields runes, not bytes
	for i, r := range "héllo" {
		fmt.Printf("%d:%c ", i, r)
	}
	fmt.Println()

	// ignore the index with _
	total := 0
	for _, v := range []int{4, 5, 6} {
		total += v
	}
	fmt.Println("total", total)
}
Outputcompiled & run with real Go
0:h 1:é 3:l 4:l 5:o 
total 15

The index jumps from 1 to 3 because é takes two bytes in UTF-8. Module 04 explains strings, bytes and runes.

Error you will hit

There is no while keyword

go
package main

import "fmt"

func main() {
	i := 0
	while i < 3 {
		fmt.Println(i)
		i++
	}
}
# command-line-arguments
./main.go:7:8: syntax error: unexpected name i at end of statement
./main.go:11:1: syntax error: non-declaration statement outside function body
Why the compiler said that

while is not a Go keyword, so the parser reads it as an ordinary name followed by another name, which is not a valid statement. The second error is a knock-on effect: once parsing is confused, the closing brace no longer matches. Always fix the first syntax error and recompile.

The fix

A while loop in Go is a for with only a condition.

go
package main

import "fmt"

func main() {
	i := 0
	for i < 3 {
		fmt.Println(i)
		i++
	}
}
Each iteration gets a fresh loop variable (Go 1.22+)
Before Go 1.22, the i in for i := ... was one variable reused for every iteration, so closures and goroutines that captured it all saw the final value — a famous bug. Since Go 1.22 each iteration has its own copy. If you maintain older code, you will still see the workaround i := i inside loops.
03

switch

A Go switch compares a value against each case from top to bottom and runs the first match only — there is no automatic fall-through, so you never write break at the end of a case. A case can list several values separated by commas. Cases do not have to be constants; they can be any expression.

A switch with no value after the keyword compares each case to true. That turns a long if / else if chain into a neat table, and it is very common in Go code.

gomain.go
package main

import "fmt"

func dayType(day string) string {
	switch day {
	case "Sat", "Sun":
		return "weekend"
	case "":
		return "unknown"
	default:
		return "weekday"
	}
}

func bmiLabel(bmi float64) string {
	switch { // no value: each case is a condition
	case bmi < 18.5:
		return "underweight"
	case bmi < 25:
		return "normal"
	default:
		return "overweight"
	}
}

func main() {
	fmt.Println(dayType("Sun"), dayType("Tue"), dayType(""))
	fmt.Println(bmiLabel(17), bmiLabel(22.4), bmiLabel(31))

	switch n := 3; n {
	case 3:
		fmt.Println("three")
		fallthrough // explicitly continue into the next case body
	case 4:
		fmt.Println("four (via fallthrough)")
	case 5:
		fmt.Println("five")
	}
}
Outputcompiled & run with real Go
weekend weekday unknown
underweight normal overweight
three
four (via fallthrough)
Your turn

Add a case to bmiLabel that returns "obese" for 30 and above. Does its position matter?

fallthrough does not check the next condition
fallthrough jumps into the next case's body unconditionally — case 4 ran above even though n is 3. It is rare in real code; listing several values in one case is almost always clearer.
Error you will hit

duplicate case in switch

go
package main

import "fmt"

func main() {
	day := 2
	switch day {
	case 1:
		fmt.Println("Mon")
	case 2:
		fmt.Println("Tue")
	case 1:
		fmt.Println("Mon again")
	}
}
# command-line-arguments
./main.go:12:7: duplicate case 1 (constant of type int) in expression switch
	./main.go:8:7: previous case
Why the compiler said that

Since only the first match ever runs, a second case 1 could never execute. The compiler catches duplicate constant cases and points at both positions.

The fix

Remove the duplicate or merge the bodies.

go
package main

import "fmt"

func main() {
	day := 2
	switch day {
	case 1:
		fmt.Println("Mon")
	case 2:
		fmt.Println("Tue")
	}
}
04

break, continue and labels

continue skips to the next iteration; break leaves the innermost for, switch or select. That last part trips people up: a break inside a switch that sits inside a loop only leaves the switch. To leave an outer loop, put a label on it and write break Label or continue Label.

gomain.go
package main

import "fmt"

func main() {
	for i := range 6 {
		if i%2 == 0 {
			continue // skip even numbers
		}
		fmt.Print(i, " ")
	}
	fmt.Println()

	grid := [][]int{{1, 2, 3}, {4, -1, 6}, {7, 8, 9}}
search:
	for r, row := range grid {
		for c, v := range row {
			if v < 0 {
				fmt.Println("negative at", r, c)
				break search // leaves BOTH loops
			}
		}
	}

	for _, cmd := range []string{"run", "quit", "never"} {
		switch cmd {
		case "quit":
			break // only leaves the switch!
		}
		fmt.Println("saw", cmd)
	}
}
Outputcompiled & run with real Go
1 3 5 
negative at 1 1
saw run
saw quit
saw never
Your turn

Put a label loop: on the last for and change the inner break to break loop. Which lines disappear from the output?

Error you will hit

label defined and not used

go
package main

import "fmt"

func main() {
outer:
	for i := 0; i < 2; i++ {
		fmt.Println(i)
	}
}
# command-line-arguments
./main.go:6:1: label outer defined and not used
Why the compiler said that

The same "no dead code" rule as unused variables and imports applies to labels. A label nothing jumps to is an error.

The fix

Delete the label, or use it with break outer / continue outer.

go
package main

import "fmt"

func main() {
	for i := 0; i < 2; i++ {
		fmt.Println(i)
	}
}
05

defer basics

defer f() schedules a call to run when the surrounding function returns — whether it returns normally, from an early return, or while panicking. It is how Go guarantees clean-up: open a file, then immediately defer file.Close() on the next line, so the close cannot be forgotten no matter how many return paths the function grows.

Two rules to remember. Deferred calls run in last-in, first-out order, like a stack. And the call's arguments are evaluated immediately, at the defer line, not when the deferred call finally runs.

gomain.go
package main

import "fmt"

func process() {
	fmt.Println("open")
	defer fmt.Println("close") // runs when process returns

	x := 1
	defer fmt.Println("deferred x =", x) // x evaluated NOW: 1
	x = 99

	for i := range 3 {
		defer fmt.Println("defer", i) // stacked: runs 2, 1, 0
	}
	fmt.Println("working, x =", x)
}

func main() {
	process()
	fmt.Println("back in main")
}
Outputcompiled & run with real Go
open
working, x = 99
defer 2
defer 1
defer 0
deferred x = 1
close
back in main
Your turn

Replace defer fmt.Println("deferred x =", x) with defer func() { fmt.Println("closure x =", x) }(). Why does it now print 99?

The pattern you will write every day
f, err := os.Open(path)
if err != nil { return err }
defer f.Close()
The same shape is used for mu.Lock() / defer mu.Unlock(), database rows, and HTTP response bodies. Module 03 covers how defer interacts with return values, and Module 07 how it recovers from panics.
Init statement
A short statement before the condition of an if or switch, e.g. if v, err := f(); err != nil. Its variables are scoped to that statement.
range
The for clause that iterates over a slice, array, string, map, channel or integer, yielding index/key and value.
Condition-less switch
switch { case x < 0: ... } — each case is a boolean expression; a clean replacement for if/else chains.
fallthrough
A statement that transfers control into the next case body of a switch, without testing its condition.
Label
A name followed by a colon placed before a loop, used by break and continue to target that outer loop.
defer
Schedules a function call to run when the enclosing function returns; deferred calls run last-in, first-out.
Quick check

What does this print? for i := range 3 { defer fmt.Print(i) } (inside main)

Quick check

Inside a for loop, a switch has a case containing a bare break. What does that break do?

Frequently asked questions

Does Go have a while loop?
No keyword for it, but for with only a condition is a while loop: for n < 100 { ... }. A bare for { ... } is an infinite loop you leave with break or return.
Do I need break at the end of each case in a Go switch?
No. Go runs only the first matching case and then leaves the switch. If you really want to continue into the next case, write fallthrough explicitly.
When does a deferred function run in Go?
When the surrounding function returns — normally, early, or during a panic. Multiple defers run in reverse order, and their arguments are evaluated at the defer statement, not when they run.

Finish the Go handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.