Control flow
Go has one loop keyword (for) and one branch keyword (if),
both without parentheses around their conditions. The surprise is
defer — schedule something to run when the function returns.
switch cases take conditions, not just constants — it's the idiomatic dispatch tool.
1 · The intuition
Go threw out what wasn't needed. There's no while — for
handles both. No do-while — you write for { ...; if ...{ break } }.
No ?: ternary — write the if. No exceptions —
panic/recover exist but are reserved for "impossible"
states.
if, else,
for, break, continue, switch,
case, default, fallthrough,
defer, return, goto, panic,
recover. Thirteen keywords. That's the entire flow toolkit.2 · Try it — the four forms of for
package main
import "fmt"
func main() {
// 1. Classic three-part — like C
for i := 0; i < 3; i++ {
fmt.Print(i, " ")
}
fmt.Println()
// 2. While-style — single condition
n := 5
for n > 0 {
fmt.Print(n, " ")
n--
}
fmt.Println()
// 3. Infinite — break out
count := 0
for {
count++
if count == 3 { break }
}
fmt.Println("counted:", count)
// 4. Range — over slices, maps, strings, channels
xs := []string{"a", "b", "c"}
for i, v := range xs {
fmt.Printf("%d=%s ", i, v)
}
fmt.Println()
}3 · if with an initialiser — the half-statement
Go's if can declare a variable scoped to the if/else block. This is
how the (value, err) pattern reads idiomatically:
package main
import (
"fmt"
"os"
)
func main() {
// Classic shape — declare in the if header
if f, err := os.Open("nonexistent.txt"); err != nil {
fmt.Println("err:", err)
} else {
defer f.Close()
// use f here
}
// f is out of scope here
// Common variant — pre-existing variable
n := 5
if doubled := n * 2; doubled > 5 {
fmt.Println("big:", doubled)
}
}4 · switch — the power tool
Go's switch doesn't fall through by default. Cases can be lists, can
be expressions, can omit the discriminant entirely (a switch on
true is a clean if/else chain).
package main
import "fmt"
func main() {
// 1. Value switch
x := 3
switch x {
case 1, 2: fmt.Println("small") // multiple values, one case
case 3, 4: fmt.Println("medium")
default: fmt.Println("large")
}
// 2. No discriminant — switch on conditions
age := 25
switch {
case age < 13: fmt.Println("kid")
case age < 20: fmt.Println("teen")
case age < 60: fmt.Println("adult")
default: fmt.Println("senior")
}
// 3. Type switch (sneak peek for day 4)
var v interface{} = 42
switch t := v.(type) {
case int: fmt.Println("int:", t)
case string: fmt.Println("str:", t)
default: fmt.Println("other")
}
}5 · defer — schedule cleanup
package main
import "fmt"
func main() {
defer fmt.Println("1 last")
defer fmt.Println("2 second-last")
defer fmt.Println("3 first")
fmt.Println("body")
}
// defers run in LIFO order: last deferred runs first.defer fmt.Println("x=", x) captures x's current value.
To see the final value, wrap in a closure:
defer func() { fmt.Println("x=", x) }().6 · break/continue with labels
package main
import "fmt"
func main() {
grid := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
target := 5
outer:
for i, row := range grid {
for j, v := range row {
if v == target {
fmt.Printf("found at (%d, %d)
", i, j)
break outer // breaks the OUTER loop
}
}
}
}Labels are rare in idiomatic Go — usually you extract the inner loop to a function and return. But for tight inner loops where extraction would obscure the algorithm (a matrix search like above), labels are clean.
7 · From the wild
func filterPods(pods []*Pod, ns string) []*Pod {
var result []*Pod
for _, p := range pods {
switch {
case p.Namespace != ns:
continue
case p.Status.Phase == PodSucceeded:
continue
case p.Status.Phase == PodFailed:
continue
}
result = append(result, p)
}
return result
}8 · Coming from another language?
| If you know… | The bridge |
|---|---|
| C / C++ / Java | No parens around conditions. switch doesn't fall through. defer is new — closest is RAII or try-finally. |
| Python | No elif — just else if. No list comprehensions — explicit for. A bare switch replaces long elif chains. |
| JavaScript | No ternary. No do-while. defer ≈ try/finally but stacks LIFO across multiple defers. |
| Rust | No match patterns on structs — Go's switch is value-based. defer is closest to Drop but explicit. |
9 · Common mistakes
- Putting
deferinside a loop. Defers stack up — they only run when the function returns.for i := 0; i < 1000; i++ { defer f.Close() }leaks 1000 file handles until the function ends. - Expecting
switchto fall through. It doesn't. Usefallthroughif you really need it (rare). - Modifying a loop variable expecting persistence.
for i := 0; i < 10; i++creates a freshieach iteration in Go 1.22+; older versions share. - Forgetting the comma-ok form for channels.
v, ok := <-chtells you if the channel is closed.v := <-chon a closed channel returns the zero value silently. - Using
goto. It exists. Don't. The state machine you think you need can be written as a switch.
10 · Exercises (~10 min)
- FizzBuzz with a single
switchon conditions (no chained if/else). - Defer trace. Predict the output of
defer fmt.Println(i)inside a 3-iteration loop. Run it. Now wrap in a closure to capture the live value. - Find in matrix. Reproduce the labelled-break exercise from section 6 with a 4×4 grid and a target value.
- Range over a string.
for i, r := range "héllo"— what type isr? What'siafter the special character? Hint: byte index, not rune index.
11 · When it clicks
- You never type
while. - You reach for
switchover chainedelse ifby reflex. defer f.Close()appears right afterf, err := os.Open(...)without thinking.- You can predict what a
defer+ closure combo will print before running it.