Frontend Labs
⋯

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.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36