# Why Control Flow Matters
Control flow is the decision-making layer of your program. It decides what runs, when it runs, and when execution stops. In Go, the surface area is intentionally small: if, for,switch, plus break/continue.
This simplicity is a strength in production code. During incidents, on-call engineers can scan control flow quickly without juggling many language constructs. Most bugs in backend services are not syntax problems — they are branching errors: missing edge-case checks, wrong loop exits, or conditions in the wrong order.
Real-World Analogyclick to expand
Think of control flow like airport operations. if is the security gate decision (allow/deny), for is the queue processing, and switch routes passengers by destination terminal.
A wrong condition can misroute thousands of requests exactly like sending passengers to the wrong gate. Clean control flow keeps systems predictable, testable, and easier to debug.
# if / else
Go's if doesn't need parentheses around the condition, but braces are always required.
package main
import "fmt"
func main() {
age := 20
if age >= 18 {
fmt.Println("Adult")
} else if age >= 13 {
fmt.Println("Teenager")
} else {
fmt.Println("Child")
}
}if with Init Statement
Go lets you run a short statement before the condition. The variable is scoped to the if/else block:
if err := doSomething(); err != nil {
fmt.Println("error:", err)
} else {
fmt.Println("success")
}# for — The Only Loop
Go has only one loop keyword: for. It replaces while, do-while, and traditional for from other languages.
Classic for loop
for i := 0; i < 5; i++ {
fmt.Println(i)
}While-style loop
n := 1
for n < 100 {
n *= 2
}
fmt.Println(n) // 128Infinite loop
for {
// runs forever until break or return
break
}for range
Iterate over slices, maps, strings, and channels:
names := []string{"Go", "Rust", "Python"}
for i, name := range names {
fmt.Printf("%d: %s\n", i, name)
}# switch
Go's switch is cleaner than C/Java: no automatic fall-through, and cases can be expressions.
day := "Tuesday"
switch day {
case "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday":
fmt.Println("Weekday")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Unknown")
}Tagless switch (like if/else chain)
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}# break, continue & Labels
break exits a loop, continue skips to the next iteration, and labels let you target outer loops precisely.
continue for filtering input
nums := []int{-2, 5, 0, 9}
for _, n := range nums {
if n <= 0 {
continue // skip non-positive values
}
fmt.Println(n) // prints 5, then 9
}Labeled break for nested loops
// Labeled break exits the outer loop
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break Outer
}
fmt.Printf("%d,%d ", i, j)
}
}
// Output: 0,0 0,1 0,2 1,0# Real-World Example: Request Validation Pipeline
This pattern combines if, for, switch, and continue the way backend services commonly do when processing batches of requests.
package main
import "fmt"
type Req struct {
UserID int
Action string
}
func handleBatch(reqs []Req) {
for _, r := range reqs {
if r.UserID <= 0 {
fmt.Println("skip invalid user")
continue
}
switch r.Action {
case "create", "update":
fmt.Println("process write for user", r.UserID)
case "delete":
fmt.Println("process delete for user", r.UserID)
default:
fmt.Println("unknown action:", r.Action)
}
}
}
func main() {
reqs := []Req{{1, "create"}, {0, "update"}, {2, "delete"}, {3, "noop"}}
handleBatch(reqs)
}This is exactly the style used in handlers, consumers, and workers: validate, skip bad data, route by operation, and keep each branch explicit.
⚡ Key Takeaways
- No parentheses around conditions, but braces are required
foris the only loop — it handles classic, while-style, infinite, and range patternsswitchauto-breaks; usefallthroughexplicitly if needed- Init statements in
ifandswitchscope variables tightly - Labels +
break/continuecontrol nested loops