Go Control Structures Tutorial
Control structures in Go determine the flow of program execution. Unlike many languages, Go keeps its control structures minimal and explicit, with only one looping construct (for) but flexible conditional branching.
1. If Statements
The if statement evaluates a condition and executes a block of code if the condition is true. Go's if has several unique characteristics.
if x > 10 {
fmt.Println("x is large")
} else if x > 5 {
fmt.Println("x is medium")
} else {
fmt.Println("x is small")
}
Key Features:
- No parentheses around conditions (unlike C-family languages)
- Braces are always required, even for single-line blocks
- Can include an optional initialization statement before the condition
Initialization Statement:
if err := process(); err != nil {
fmt.Println("Error:", err)
}
The initialization statement (before the semicolon) limits the variable's scope to the if-else block, a clean way to handle temporary variables.
If Statement Quiz
What happens to variables declared in the if initialization?
2. For Loops
Go has only one looping construct - the for loop - that serves three purposes: traditional loops, while loops, and infinite loops.
Traditional For Loop:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
While-Style Loop:
count := 0
for count < 5 {
fmt.Println(count)
count++
}
Infinite Loop:
for {
fmt.Println("Looping forever")
break // Needed to exit
}
Range-Based Loop:
nums := []int{2, 4, 6}
for index, value := range nums {
fmt.Println(index, value)
}
Key Notes:
- All four forms use the
forkeyword - No parentheses around conditions
breakexits the loop,continueskips to next iteration- Range loops work with arrays, slices, strings, maps, and channels
For Loop Quiz
How do you write a while loop in Go?
3. Switch Statements
Go's switch is more flexible than C-style switches, with several important differences.
Basic Switch:
switch day {
case "Mon":
fmt.Println("Monday")
case "Tue":
fmt.Println("Tuesday")
default:
fmt.Println("Other day")
}
Switch with Initializer:
switch time.Now().Weekday() {
case time.Saturday, time.Sunday:
fmt.Println("Weekend")
default:
fmt.Println("Weekday")
}
Type Switch:
switch v := x.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
Key Differences from C/Java:
- No fallthrough between cases (unless explicitly using
fallthrough) - Cases don't need to be constants
- Can switch on any type, not just integers
- Default case is optional
Switch Quiz
What happens by default after a case matches?
4. Defer Statement
The defer statement postpones execution of a function until the surrounding function returns, making it ideal for cleanup tasks.
func readFile() {
file, _ := os.Open("data.txt")
defer file.Close()
// File operations here
// file.Close() will be called automatically
}
Key Characteristics:
- Arguments to deferred functions are evaluated immediately
- Defers execute in LIFO (last-in-first-out) order
- Commonly used for resource cleanup (files, locks, connections)
- Works with named return values (can modify return values)
Multiple Defers Example:
func example() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
fmt.Println("Function body")
// Output:
// Function body
// Second defer
// First defer
}
Defer Quiz
When are deferred functions executed?
5. Panic and Recover
Go uses panic and recover for exceptional error handling, though they're rarely needed in normal operation.
Panic:
func riskyOperation() {
panic("Something went terribly wrong")
}
Recover:
func safeCall() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}()
riskyOperation()
}
Key Points:
panicstops normal execution and begins panickingrecovercan stop the panic and return the panic value- Only useful inside deferred functions
- Not for normal error handling - prefer returning errors
Panic Quiz
Where must recover be called to catch a panic?
6. Select Statement
The select statement lets a goroutine wait on multiple communication operations, primarily used with channels.
select {
case msg := <-channel1:
fmt.Println("Received", msg)
case channel2 <- "hello":
fmt.Println("Sent hello")
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
default:
fmt.Println("No activity")
}
Key Features:
- Blocks until one of the cases can run
- If multiple cases are ready, one is chosen at random
- The
defaultcase runs immediately if no other case is ready - Often used with timeouts using
time.After
Select Quiz
What happens if multiple select cases are ready?
7. Common Patterns
Go developers use several idiomatic control flow patterns for common scenarios.
Error Handling Pattern:
if err := doSomething(); err != nil {
// Handle error
return err
}
// Continue with success case
Worker Pool Pattern:
for task := range tasks {
go func(t Task) {
// Process task
}(task)
}
Timeout Pattern:
select {
case result := <-operationChan:
// Use result
case <-time.After(2 * time.Second):
// Handle timeout
}
Key Idioms:
- Early returns for error handling
- Using channels for synchronization
- Context package for cancellation
- Worker pools for controlled concurrency
Patterns Quiz
What's the preferred error handling style in Go?