Control Flow
Control Flow
Imagine you're a chef cooking: if the pan is hot, add oil (if); choose different cooking methods based on the ingredient (switch); keep stirring until it's done (for); don't forget to turn off the heat before plating (defer). Programs work the same way — control flow determines the execution path of code, teaching computers how to "make decisions" and "repeat tasks".
1. Core Concepts
Go's control flow statements are concise yet powerful, mainly including the following categories:
1.1 if / else Conditional
Go's if statement has a unique feature: you can add an initialization statement before the condition, separated by a semicolon ;. The variable's scope is limited to the if/else block.
// Basic form
if condition {
// Execute when condition is true
} else if otherCondition {
// Execute when other condition is true
} else {
// Execute when none of the above match
}
// With initialization statement
if initStatement; condition {
// Initialized variable is only visible within the if/else block
}
1.2 switch Statement
Go's switch has two key differences from other languages:
- Automatic break: Each
caseterminates automatically and does not "fall through" to the next case. - Type switch support: You can check the type of a variable.
switch variable {
case value1:
// Handle value1
case value2, value3: // Multiple values separated by commas
// Handle value2 or value3
default:
// Default handling
}
1.3 for Loop
for is Go's only loop construct. It replaces while, do-while, and other loops found in other languages.
| Form | Syntax | Use Case |
|---|---|---|
| Traditional for | for init; cond; post {} |
Known iteration count |
| While-style | for cond {} |
Conditional loop |
| Infinite loop | for {} |
Continuous execution |
| Range iteration | for i, v := range collection {} |
Iterating collections |
1.4 break and continue
break: Immediately exits the current loop.continue: Skips the remaining code in the current iteration and proceeds to the next one.
1.5 defer (Delayed Execution)
The defer statement postpones a function call until the surrounding function returns. Multiple defer calls execute in last-in, first-out (LIFO) stack order.
func example() {
defer fmt.Println("Registered first, executed last")
defer fmt.Println("Registered second, executed second-to-last")
fmt.Println("Normal execution")
}
// Output order: Normal execution → Registered second → Registered first
2. Basic Syntax/Usage
if / else Usage
// Standard if/else
score := 85
if score >= 90 {
fmt.Println("Excellent")
} else if score >= 80 {
fmt.Println("Good")
} else if score >= 60 {
fmt.Println("Pass")
} else {
fmt.Println("Fail")
}
if conditions do not need parentheses, but curly braces {} are required, and else must be on the same line as the closing brace of if. This is a Go syntax rule — not following it will cause a compilation error.
// if with initialization statement
// err's scope is limited to the if/else block; it cannot be accessed outside
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
}
if with initialization statements is very common in Go, especially for error handling. It minimizes variable scope and avoids polluting the outer scope.
switch Usage
// Basic switch
day := "Wednesday"
switch day {
case "Monday":
fmt.Println("New week begins")
case "Tuesday", "Wednesday", "Thursday":
fmt.Println("Weekday")
case "Friday":
fmt.Println("Almost weekend")
case "Saturday", "Sunday":
fmt.Println("Happy weekend")
default:
fmt.Println("Invalid day")
}
switch doesn't require break after each case. If you really need to "fall through" to the next case, use the fallthrough keyword, but this is rare.
// Tagless switch (replaces long if-else chains)
score := 85
switch {
case score >= 90:
fmt.Println("Excellent")
case score >= 80:
fmt.Println("Good")
case score >= 60:
fmt.Println("Pass")
default:
fmt.Println("Fail")
}
switch is not followed by a variable, it's equivalent to switch true and can replace lengthy if-else chains for cleaner code.
for Loop Usage
// Traditional for loop
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style
count := 0
for count < 5 {
fmt.Println(count)
count++
}
// Infinite loop (use with break)
for {
fmt.Println("Press Ctrl+C to exit")
break // Using break here to avoid a true infinite loop
}
defer Usage
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
fmt.Println("Failed to open file:", err)
return
}
defer file.Close() // Ensure file is closed when function ends
// ... read file contents ...
}
defer arguments are evaluated immediately when the defer statement appears, not when the function executes. Be aware of this detail to avoid unexpected behavior.
Example 1: Basic Usage (Difficulty ⭐)
This example demonstrates basic usage of if/else and switch.
package main
import "fmt"
func main() {
// ===== if/else example: determine grade =====
score := 78
// Using if with initialization statement
// rank variable is only valid within the if/else block
if rank := score / 10; rank >= 9 {
fmt.Printf("Score %d, Grade: Excellent\n", score)
} else if rank >= 8 {
fmt.Printf("Score %d, Grade: Good\n", score)
} else if rank >= 6 {
fmt.Printf("Score %d, Grade: Pass\n", score)
} else {
fmt.Printf("Score %d, Grade: Fail\n", score)
}
// ===== switch example: determine season by month =====
month := 8
switch month {
case 3, 4, 5:
fmt.Printf("Month %d is Spring\n", month)
case 6, 7, 8:
fmt.Printf("Month %d is Summer\n", month)
case 9, 10, 11:
fmt.Printf("Month %d is Autumn\n", month)
case 12, 1, 2:
fmt.Printf("Month %d is Winter\n", month)
default:
fmt.Printf("%d is not a valid month\n", month)
}
// ===== Tagless switch: replaces if-else chain =====
hour := 14
switch {
case hour < 6:
fmt.Println("Dawn")
case hour < 12:
fmt.Println("Morning")
case hour < 18:
fmt.Println("Afternoon")
default:
fmt.Println("Evening")
}
}
Output:
Score 78, Grade: Pass
Month 8 is Summer
Afternoon
Key Points:
rank := score / 10is an initialization statement;rank's scope is limited to theif/elseblockswitch'scase 3, 4, 5means matching any of 3, 4, or 5- Tagless
switchis suitable for replacing complexif-elsechains
Example 2: Intermediate Usage (Difficulty ⭐⭐)
This example demonstrates various forms of for loops and usage of break / continue.
package main
import "fmt"
func main() {
// ===== Traditional for loop: sum of 1 to 100 =====
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Printf("Sum of 1 to 100: %d\n", sum)
// ===== range over slice =====
fruits := []string{"Apple", "Banana", "Orange", "Grape"}
for index, fruit := range fruits {
fmt.Printf("Index %d: %s\n", index, fruit)
}
// ===== range over string (by rune) =====
text := "GoLang"
for i, ch := range text {
fmt.Printf("Position %d: %c (Unicode: %U)\n", i, ch, ch)
}
// ===== break: find the first number divisible by 7 =====
for i := 1; i <= 100; i++ {
if i%7 == 0 {
fmt.Printf("First number divisible by 7: %d\n", i)
break // Exit loop immediately after finding it
}
}
// ===== continue: print all odd numbers from 1-20 =====
fmt.Print("Odd numbers 1-20: ")
for i := 1; i <= 20; i++ {
if i%2 == 0 {
continue // Skip even numbers, go to next iteration
}
fmt.Printf("%d ", i)
}
fmt.Println()
// ===== Labeled break: break out of outer loop =====
fmt.Println("Searching for number 5 in a 2D array:")
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
found: // Label name
for row, cols := range matrix {
for col, val := range cols {
if val == 5 {
fmt.Printf("Found! Position: row %d, col %d\n", row, col)
break found // Break out of outer loop
}
}
}
}
Output:
Sum of 1 to 100: 5050
Index 0: Apple
Index 1: Banana
Index 2: Orange
Index 3: Grape
Position 0: G (Unicode: U+0047)
Position 1: o (Unicode: U+006F)
Position 2: L (Unicode: U+004C)
Position 3: a (Unicode: U+0061)
Position 4: n (Unicode: U+006E)
Position 5: g (Unicode: U+0067)
First number divisible by 7: 7
Odd numbers 1-20: 1 3 5 7 9 11 13 15 17 19
Searching for number 5 in a 2D array:
Found! Position: row 1, col 1
Key Points:
for i := 1; i <= 100; i++is the standard three-part looprangeover a slice returns index and value; over a string it iterates by Unicode character (rune)break founduses a label to break out of a specified outer loop, very useful in nested loops
Example 3: Comprehensive Application (Difficulty ⭐⭐⭐)
This example combines all control flow statements to implement a simple student grade management system.
package main
import "fmt"
// Student struct
type Student struct {
Name string
Scores []int
}
// GetAverage calculates the average score
func (s Student) GetAverage() float64 {
if len(s.Scores) == 0 {
return 0
}
total := 0
for _, score := range s.Scores {
total += score
}
return float64(total) / float64(len(s.Scores))
}
// GetGrade returns the grade based on average score
func (s Student) GetGrade() string {
avg := s.GetAverage()
switch {
case avg >= 90:
return "A (Excellent)"
case avg >= 80:
return "B (Good)"
case avg >= 70:
return "C (Average)"
case avg >= 60:
return "D (Pass)"
default:
return "F (Fail)"
}
}
// PrintReport prints the grade report
func PrintReport(students []Student) {
fmt.Println("========================================")
fmt.Println(" Student Grade Report")
fmt.Println("========================================")
// Use defer to ensure the report ending prints after all student data
defer fmt.Println("========================================")
defer fmt.Println(" Report Complete")
for i, student := range students {
// Use defer to demonstrate LIFO order
defer func(name string, idx int) {
fmt.Printf("[Cleanup] Finished processing %s's report\n", name)
}(student.Name, i)
// Print student info
fmt.Printf("\nStudent %d: %s\n", i+1, student.Name)
fmt.Printf(" Scores: %v\n", student.Scores)
fmt.Printf(" Average: %.1f\n", student.GetAverage())
fmt.Printf(" Grade: %s\n", student.GetGrade())
// Analyze each subject score
subjects := []string{"Chinese", "Math", "English"}
for j, score := range student.Scores {
// Ensure index is within bounds
subject := "Unknown"
if j < len(subjects) {
subject = subjects[j]
}
// Check each subject score
if score < 60 {
fmt.Printf(" ⚠ %s (%d) failed, needs retake\n", subject, score)
}
}
}
}
// FindTopStudent finds the student with the highest average
func FindTopStudent(students []Student) (string, float64) {
if len(students) == 0 {
return "", 0
}
topName := students[0].Name
topAvg := students[0].GetAverage()
for _, s := range students[1:] {
if avg := s.GetAverage(); avg > topAvg {
topName = s.Name
topAvg = avg
}
}
return topName, topAvg
}
func main() {
// Create student data
students := []Student{
{"Alice", []int{85, 92, 78}},
{"Bob", []int{90, 95, 88}},
{"Charlie", []int{72, 58, 65}},
{"Diana", []int{95, 98, 92}},
}
// Print grade report
PrintReport(students)
// Find top student
topName, topAvg := FindTopStudent(students)
fmt.Printf("\n🏆 Top Student: %s (Average: %.1f)\n", topName, topAvg)
}
Output:
========================================
Student Grade Report
========================================
Student 1: Alice
Scores: [85 92 78]
Average: 85.0
Grade: B (Good)
Student 2: Bob
Scores: [90 95 88]
Average: 91.0
Grade: A (Excellent)
Student 3: Charlie
Scores: [72 58 65]
Average: 65.0
Grade: D (Pass)
⚠ Math (58) failed, needs retake
Student 4: Diana
Scores: [95 98 92]
Average: 95.0
Grade: A (Excellent)
[Cleanup] Finished processing Diana's report
[Cleanup] Finished processing Charlie's report
[Cleanup] Finished processing Bob's report
[Cleanup] Finished processing Alice's report
========================================
Report Complete
========================================
🏆 Top Student: Diana (Average: 95.0)
Key Points:
- Multiple
deferregistrations execute in LIFO (last-in, first-out) order: Diana → Charlie → Bob → Alice defer fmt.Println("Report Complete")was registered first but executes last (because it was registered earliest, it's popped last)- Tagless
switchreplaces complexif-elsechains for grade determination for rangeiterates over a slice of structs;for i, s := range students[1:]starts comparing from the second elementdeferin anonymous functions immediately captures parameter values (passes copies ofstudent.Nameandi)
3. Common Use Cases
Case 1: Error Handling Chain (if + initialization statement)
In Go, the most common use of if with initialization statements is error handling:
package main
import (
"fmt"
"strconv"
)
func main() {
// Parse numbers and handle errors
inputs := []string{"42", "abc", "100", "xyz", "0"}
for _, input := range inputs {
// Perform type conversion in initialization, check for error in condition
if num, err := strconv.Atoi(input); err != nil {
fmt.Printf("❌ Cannot parse %q: %v\n", input, err)
} else {
fmt.Printf("✅ Parsed successfully: %q → %d\n", input, num)
}
}
}
Case 2: Iterate Map with Conditional Filtering (for + switch)
package main
import "fmt"
func main() {
// Student grade table
students := map[string]int{
"Alice": 85,
"Bob": 92,
"Charlie": 58,
"Diana": 76,
"Eve": 45,
}
// Count students in each grade level
excellent, good, pass, fail := 0, 0, 0, 0
for name, score := range students {
switch {
case score >= 90:
excellent++
fmt.Printf("⭐ %s: %d (Excellent)\n", name, score)
case score >= 80:
good++
fmt.Printf("👍 %s: %d (Good)\n", name, score)
case score >= 60:
pass++
fmt.Printf("✅ %s: %d (Pass)\n", name, score)
default:
fail++
fmt.Printf("❌ %s: %d (Fail)\n", name, score)
}
}
fmt.Printf("\nSummary: Excellent %d, Good %d, Pass %d, Fail %d\n",
excellent, good, pass, fail)
}
❓ FAQ
Q1: Why doesn't Go's switch need break?
Go's designers believed that C's switch fallthrough behavior was a common source of bugs. Go's switch automatically exits after each case, preventing accidental fallthrough from forgotten break statements. If fallthrough is truly needed, you can explicitly use the fallthrough keyword.
// fallthrough usage (rare)
x := 1
switch x {
case 1:
fmt.Println("one")
fallthrough // Continue executing the next case's code
case 2:
fmt.Println("two") // Will be executed
default:
fmt.Println("other")
}
// Output: one \n two
Q2: Are variables in for range copies?
Yes. The value variable in for range is a copy of the element; modifying it won't affect the original collection. If you need to modify elements in the original collection, access them by index.
nums := []int{1, 2, 3, 4, 5}
// ❌ Wrong: modifying a copy
for _, n := range nums {
n *= 2 // Won't affect nums
}
// ✅ Correct: modify original slice via index
for i := range nums {
nums[i] *= 2 // Will modify nums
}
Q3: When are defer arguments evaluated?
defer arguments are evaluated when the defer statement appears, not when the function returns.
func main() {
x := 10
defer fmt.Println("x in defer:", x) // x is evaluated here as 10
x = 20
fmt.Println("x after modification:", x)
}
// Output:
// x after modification: 20
// x in defer: 10 (not 20!)
Q4: How to correctly capture loop variables when using goroutines in a loop?
Before Go 1.22, for loop variables shared the same address across all iterations, which could cause issues in goroutines. Go 1.22+ fixed this behavior — each iteration gets its own variable. If using an older version, pass the variable as an argument:
// Go 1.22+ — just use directly
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // Each goroutine gets its own i
}()
}
// Older versions — explicitly pass as argument
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // n is a copy of i
}(i)
}
📖 Summary
if/elsesupports initialization statements (if init; cond {}); variable scope is limited to the blockswitchdoes not need break by default; each case terminates automatically; supports tagless switch as an alternative to if-else chainsforis Go's only loop, with four forms: traditional for, while-style, infinite loop, range iterationbreakcan be used with labels to break out of specific loop levels;continueskips the current iterationdeferexecutes in last-in, first-out (LIFO) order; arguments are evaluated immediately at the defer statementrangereturns copies; modifying copies does not affect the original collection
📝 Exercises
Exercise 1 (⭐)
Write a program that takes a year as input and determines whether it's a leap year. Rules:
- A year is a leap year if it's divisible by 4 but not by 100, or if it's divisible by 400.
- Use the
ifwith initialization statement form.
package main
import "fmt"
func main() {
year := 2024 // Change this value to test
// Write your code here
// Hint: Use if init; cond form
}
Exercise 2 (⭐⭐)
Write a program that uses for loops to print a multiplication table. Format requirements:
- One multiplicand per row
- Use tab
\tfor alignment - Combine
forloops and formatted output
package main
import "fmt"
func main() {
// Write your code here
// Hint: Use two nested for loops
// Outer loop i from 1 to 9
// Inner loop j from 1 to i
}
Exercise 3 (⭐⭐⭐)
Write a program that implements a simple number guessing game:
- Use a loop to let the user guess repeatedly until correct
- After each guess, give a hint: "too high" or "too low"
- Use
deferto record the game's start and end times - Use
switchto give different ratings based on the number of guesses (1-3: genius, 4-6: good, 7+: keep trying)
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
// Use defer to record end time
start := time.Now()
defer func() {
fmt.Printf("\nGame duration: %v\n", time.Since(start))
}()
// Generate random number 1-100
target := rand.Intn(100) + 1
attempts := 0
fmt.Println("=== Number Guessing Game ===")
fmt.Println("I'm thinking of a number between 1-100, try to guess it!")
// Write your code here
// Hints:
// 1. Use a for loop to keep the game going
// 2. Use fmt.Scan to read user input
// 3. Use switch to check guess result and rating level
// 4. Use break to exit the loop when guessed correctly
}
Next Lesson
Next Lesson: Functions → Learn Go's function definitions, multiple return values, variadic parameters, anonymous functions, closures, and other core concepts.



