4. Control flow: if, switch, for, defer
4.1 if with an initializer
if v, err := parse(input); err != nil {
return fmt.Errorf("parse: %w", err)
} else {
use(v)
}
v and err are scoped to the if/else block only — a deliberate
narrowing that keeps error-carrying temporaries from leaking into the rest
of the function.
4.2 switch without fallthrough
Unlike C, Go's switch does not fall through by default — each case
implicitly breaks. Use the fallthrough keyword if you actually want the
old behavior. switch also works without an expression at all (a cleaner
if/else if chain) and with a type assertion (switch v := x.(type),
covered in Chapter 9).
switch status := resp.StatusCode; {
case status >= 500:
log.Println("server error")
case status >= 400:
log.Println("client error")
case status >= 300:
log.Println("redirect")
default:
log.Println("ok")
}
4.3 for is the only loop
There's no while, no do-while — for covers every case:
for i := 0; i < 10; i++ { } // classic
for cond { } // while
for { } // infinite (use break)
for i, v := range items { } // range over slice/array/map/string/channel
for range ch { } // discard the value, just drain the channel
4.4 The loop-variable capture trap (fixed in Go 1.22)
Before Go 1.22, for-loop variables were shared across iterations, which
made this a classic bug:
// Go < 1.22: prints "3 3 3" — all goroutines captured the same `i`
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }()
}
Go 1.22+ gives each iteration its own i, so this now correctly prints
0 1 2 (in some order). Know both behaviors — you will read code
written for both, and go.mod's go directive controls which semantics
apply.
4.5 defer: cleanup that always runs, in LIFO order
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // runs when processFile returns, however it returns
// ... use f ...
return nil
}
Three rules to internalize: deferred calls run in LIFO order; their
arguments are evaluated immediately when defer executes, not when the
deferred call finally runs; and defer runs even if the function panics —
which is exactly how recover() (Chapter 30) gets a chance to run.
func demo() {
for i := 0; i < 3; i++ {
defer fmt.Println("deferred:", i) // captures i's value *now*
}
fmt.Println("done")
}
// prints: done / deferred: 2 / deferred: 1 / deferred: 0
4.6 Real-world example
Many HTTP handlers that open a database transaction follow this shape:
check Begin/BeginTx, defer tx.Rollback(), then tx.Commit() on the
success path. If the handler returns early on any error, the deferred
Rollback() cleans up the still-open transaction; after a successful
Commit, Rollback returns an error such as "transaction already
committed" and is typically ignored.