Control Flow Overview
Go provides a simple set of control flow constructs that are powerful enough to build complex programs. Unlike many languages, Go has only one looping construct: the for loop.
Go's Philosophy: Less is More
Go deliberately omits many control flow features found in other languages:
- No while loops - Use for with a condition
- No do-while loops - Use for with break
- No ternary operator (?:) - Use if-else for clarity
- No foreach - Use for-range instead
This minimalist approach reduces complexity and improves code readability.
Control Flow Fundamentals
Control flow determines the order in which statements execute in your program. Go's control flow constructs follow these principles:
- Explicit is better than implicit: No automatic fallthrough in switch statements
- One way to do things: Single loop construct reduces cognitive load
- Clean scoping: Variables declared in control structures are scoped to those blocks
- Error handling first: Idiomatic Go checks errors immediately
Decision Flow Patterns
Go programs typically follow these decision flow patterns:
Pattern | Use Case | Example |
---|---|---|
Guard Clause | Early return for error conditions | if err != nil { return err } |
Happy Path | Main logic at the end | Handle errors first, then process |
Table-Driven | Multiple conditions with actions | Map or slice of handlers |
If Statements
Understanding Boolean Logic
If statements evaluate boolean expressions to determine execution flow. Go uses short-circuit evaluation, meaning it stops evaluating as soon as the result is determined.
Short-Circuit Evaluation
In expressions using &&
(AND) and ||
(OR):
&&
stops at the first false condition||
stops at the first true condition
This allows safe patterns like: if ptr != nil && ptr.value > 0
Basic If
if x > 0 { fmt.Println("x is positive") } // If-else if age >= 18 { fmt.Println("Adult") } else { fmt.Println("Minor") } // If-else if-else if score >= 90 { grade = "A" } else if score >= 80 { grade = "B" } else if score >= 70 { grade = "C" } else { grade = "F" }
If with Short Statement
Go allows a short statement before the condition, creating variables scoped to the if block. This pattern reduces variable scope and improves code clarity.
// Variable scoped to if block if err := doSomething(); err != nil { return err } // Common pattern for map access if val, ok := myMap[key]; ok { fmt.Printf("Value: %v ", val) } else { fmt.Println("Key not found") }
Switch Statements
Switch Theory and Design
Go's switch statement is more flexible and powerful than in many C-like languages. Key differences:
- No automatic fallthrough: Each case is independent by default
- Multiple values per case: Can test against multiple conditions
- Any comparable type: Not limited to integers
- No break needed: Cases don't fall through automatically
Switch vs If-Else Performance
For 3+ conditions, switch statements are generally faster than if-else chains. The Go compiler can optimize switches into jump tables or binary searches, while if-else chains always evaluate sequentially.
Basic Switch
switch day { case "Monday": fmt.Println("Start of work week") case "Friday": fmt.Println("TGIF!") case "Saturday", "Sunday": fmt.Println("Weekend!") default: fmt.Println("Midweek") }
Switch without Expression
// Like if-else chain switch { case score >= 90: grade = "A" case score >= 80: grade = "B" case score >= 70: grade = "C" default: grade = "F" } // Type switch switch v := x.(type) { case int: fmt.Printf("Integer: %d ", v) case string: fmt.Printf("String: %s ", v) case bool: fmt.Printf("Boolean: %t ", v) default: fmt.Printf("Unknown type ") }
For Loops
The Universal Loop Construct
Go uses a single for
keyword for all loops, replacing while, do-while, and foreach
from other languages. This unification simplifies the language while maintaining full expressiveness.
Three Forms of For Loop
- Complete form:
for init; condition; post { }
- Condition only:
for condition { }
(like while) - Infinite loop:
for { }
(exit with break/return)
Loop Performance Considerations
- Pre-calculate lengths: Store len() results outside loops when possible
- Range vs index: Range can be slower for large slices if you only need the index
- Avoid allocations: Declare variables outside loops when reusable
- Buffer channels: Use buffered channels in loops to avoid blocking
Traditional For Loop
// Classic for loop for i := 0; i < 10; i++ { fmt.Println(i) } // While-style loop count := 0 for count < 5 { fmt.Println(count) count++ } // Infinite loop for { // Break or return to exit if condition { break } }
Range Loops
⚠️ Range Variable Capture Pitfall
The loop variables in a range are reused on each iteration. This can cause bugs when capturing them in goroutines or closures. Always copy the value or use the index to access the original.
Range provides a clean way to iterate over slices, arrays, maps, strings, and channels. It returns one or two values depending on what you're iterating over.
// Range over slice numbers := []int{1, 2, 3, 4, 5} for index, value := range numbers { fmt.Printf("%d: %d ", index, value) } // Range over map ages := map[string]int{"Alice": 30, "Bob": 25} for name, age := range ages { fmt.Printf("%s is %d years old ", name, age) } // Range over string (runes) for i, char := range "Hello, 世界" { fmt.Printf("%d: %c ", i, char) }
Defer Statements
Understanding Defer Mechanics
Defer pushes function calls onto a stack that executes in LIFO (Last In, First Out) order when the surrounding function returns. This guarantees cleanup even if a panic occurs.
Defer Execution Timing
- Arguments evaluated immediately: Function arguments are evaluated when defer is called
- Function executed later: The function itself runs when the surrounding function returns
- Multiple defers: Execute in reverse order (stack behavior)
- Runs on panic: Deferred functions still execute during panic unwinding
⚠️ Defer in Loops
Avoid defer in loops unless necessary. Deferred functions don't execute until the function returns, potentially accumulating many deferred calls and consuming memory.
// Bad: Accumulates file handles for _, file := range files { f, _ := os.Open(file) defer f.Close() // Won't close until function ends! } // Good: Close immediately for _, file := range files { func() { f, _ := os.Open(file) defer f.Close() // Closes when anonymous func ends // Process file }() }
Common Defer Patterns
// Defer executes when function returns func readFile() { file, err := os.Open("file.txt") if err != nil { return } defer file.Close() // Ensures file is closed // Read file content } // Multiple defers (LIFO order) func example() { defer fmt.Println("Third") defer fmt.Println("Second") defer fmt.Println("First") // Output: First, Second, Third }
Break and Continue
// Break exits the loop for i := 0; i < 10; i++ { if i == 5 { break } fmt.Println(i) } // Continue skips iteration for i := 0; i < 10; i++ { if i%2 == 0 { continue } fmt.Println(i) // Only odd numbers } // Labels for nested loops 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) } }
Control Flow Best Practices
Idiomatic Go Patterns
Guard Clauses and Early Returns
Handle error cases first and return early. This keeps the happy path at the main indentation level.
// Good: Early return pattern func process(data []byte) error { if data == nil { return errors.New("data is nil") } if len(data) == 0 { return errors.New("data is empty") } // Happy path with minimal nesting return doProcess(data) }
Common Anti-Patterns to Avoid
- Empty else blocks: If else is empty, use just if with negated condition
- Redundant else after return: Else is unnecessary after return/break/continue
- Complex boolean expressions: Extract into well-named functions
- Deep nesting: Refactor using early returns or separate functions
Table-Driven Tests Pattern
For complex conditional logic, consider table-driven approaches for clarity and maintainability:
// Table-driven approach for complex conditions type rule struct { condition func(int) bool action func() } rules := []rule{ {condition: func(x int) bool { return x < 0 }, action: handleNegative}, {condition: func(x int) bool { return x == 0 }, action: handleZero}, {condition: func(x int) bool { return x > 100 }, action: handleLarge}, } for _, r := range rules { if r.condition(value) { r.action() break } }
🎯 Practice Exercises
Exercise 1: FizzBuzz
Implement the classic FizzBuzz problem using Go's control flow.
Exercise 2: Number Guessing Game
Create a game where the user guesses a random number with hints.
Exercise 3: Pattern Printer
Use nested loops to print various patterns (triangles, diamonds).