Control Flow in Go
Go has a minimal set of control flow statements: if, for, and switch. There's no while loop or ternary operator.
If Statements
Basic If
if x > 10 {
fmt.Println("x is greater than 10")
}
Note: Braces {} are always required, and the opening brace must be on the same line.
If-Else
if x > 10 {
fmt.Println("greater")
} else {
fmt.Println("less or equal")
}
If-Else If-Else
if x > 10 {
fmt.Println("greater than 10")
} else if x > 5 {
fmt.Println("greater than 5")
} else {
fmt.Println("5 or less")
}
If with Initialization Statement
Go allows a short statement before the condition. The variable is scoped to the if block:
if err := doSomething(); err != nil {
fmt.Println("Error:", err)
return
}
// err is not accessible here
// Common pattern: checking map values
if value, ok := myMap[key]; ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Not found")
}
// Type assertion
if str, ok := value.(string); ok {
fmt.Println("It's a string:", str)
}
For Loops
Go has only one looping construct: for. It covers all use cases.
Traditional For Loop
for i := 0; i < 10; i++ {
fmt.Println(i)
}
While-Style Loop
// Omit init and post statements
i := 0
for i < 10 {
fmt.Println(i)
i++
}
Infinite Loop
for {
// Runs forever until break or return
if condition {
break
}
}
For-Range Loop
The most common way to iterate over collections:
// Slices and arrays
nums := []int{1, 2, 3, 4, 5}
for index, value := range nums {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// Ignore index
for _, value := range nums {
fmt.Println(value)
}
// Ignore value (just need index)
for index := range nums {
fmt.Println(index)
}
// Strings (iterates over runes, not bytes)
for index, runeValue := range "Hello, 世界" {
fmt.Printf("%d: %c\n", index, runeValue)
}
// Maps
m := map[string]int{"a": 1, "b": 2}
for key, value := range m {
fmt.Printf("%s: %d\n", key, value)
}
// Just keys
for key := range m {
fmt.Println(key)
}
// Channels
ch := make(chan int)
for value := range ch {
fmt.Println(value) // Receives until channel is closed
}
Break and Continue
// Break exits the loop
for i := 0; i < 10; i++ {
if i == 5 {
break // Exit loop when i is 5
}
fmt.Println(i)
}
// Continue skips to next iteration
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // Skip even numbers
}
fmt.Println(i) // Only prints 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 // Breaks out of both loops
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
// Also works with continue
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue outer // Skips to next i
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Switch Statements
Go's switch is more powerful and flexible than in many languages.
Basic Switch
switch day {
case "Monday":
fmt.Println("Start of work week")
case "Friday":
fmt.Println("TGIF!")
case "Saturday", "Sunday": // Multiple values
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}
Note: No break needed! Go's switch cases don't fall through by default.
Switch with Initialization
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("macOS")
case "linux":
fmt.Println("Linux")
default:
fmt.Println(os)
}
Switch with No Condition
A switch without a condition is the same as switch true. This is a clean way to write long if-else chains:
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon!")
default:
fmt.Println("Good evening!")
}
Fallthrough
If you need C-style fallthrough (rare):
switch n {
case 1:
fmt.Println("One")
fallthrough // Continue to next case
case 2:
fmt.Println("Two") // Prints for both 1 and 2
case 3:
fmt.Println("Three")
}
Type Switch
Used with interfaces to check the underlying type:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
case []int:
fmt.Printf("Slice of ints: %v\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
describe(42) // Integer: 42
describe("hello") // String: hello
describe(true) // Boolean: true
describe([]int{1}) // Slice of ints: [1]
describe(3.14) // Unknown type: float64
Defer
defer schedules a function call to run after the current function returns. Deferred calls are executed in LIFO (last-in, first-out) order.
Basic Defer
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
// Output:
// hello
// world
Common Use Cases
// Resource cleanup
func readFile(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Will run when function returns
// Read file...
return nil
}
// Mutex unlocking
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
// Timing functions
func process() {
start := time.Now()
defer func() {
fmt.Printf("Took %v\n", time.Since(start))
}()
// Do work...
}
Defer Gotchas
// Arguments are evaluated immediately
func main() {
x := 10
defer fmt.Println(x) // Prints 10, not 20
x = 20
}
// But you can use closures to capture current values
func main() {
x := 10
defer func() {
fmt.Println(x) // Prints 20 (captures variable, not value)
}()
x = 20
}
// LIFO order
func main() {
for i := 0; i < 3; i++ {
defer fmt.Println(i)
}
}
// Output:
// 2
// 1
// 0
Defer with Named Return Values
func double(x int) (result int) {
defer func() {
result *= 2 // Modifies the return value
}()
return x
}
fmt.Println(double(5)) // 10, not 5
Panic and Recover
Go doesn't have exceptions. For truly exceptional situations, there's panic and recover.
Panic
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
}
return a / b
}
When a panic occurs:
- Function execution stops
- Deferred functions are executed
- Control returns to the caller
- Process repeats up the stack
- Program crashes with a stack trace
Recover
recover catches a panic and returns normal execution:
func safeOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}()
panic("something went wrong")
}
func main() {
safeOperation()
fmt.Println("Program continues") // This runs
}
When to Use Panic
- Truly unrecoverable errors (programming bugs, corrupted state)
- During initialization when a critical resource can't be loaded
- Never for normal error handling (use error returns instead)
// Example: configuration that must exist
func mustGetEnv(key string) string {
value := os.Getenv(key)
if value == "" {
panic(fmt.Sprintf("required environment variable %s not set", key))
}
return value
}
Goto (Rarely Used)
Go has goto, but it's rarely needed:
func example() {
i := 0
loop:
if i < 5 {
fmt.Println(i)
i++
goto loop
}
}
Generally avoid goto; use for, break, and continue instead.
Next Steps
Continue to 04-functions.md to learn about functions, multiple return values, and closures.