Go has only a handful of control structures and no parentheses around their conditions. The body braces are always required.
if
if x > 0 {
...
} else if x == 0 {
...
} else {
...
}
if may include a short statement that scopes a variable to the if/else
chain only — extremely common for error handling:
if err := doSomething(); err != nil {
return err
}
// err is not in scope here
for — the only loop
for is the only looping construct. It has four forms:
// 1. Classic 3-clause C-style
for i := 0; i < 10; i++ { ... }
// 2. While-style
for cond { ... }
// 3. Infinite
for { ... } // break to exit
// 4. Range
for i, v := range slice { ... }
for k, v := range mapVar { ... }
for i := range arr { ... } // ignore the value
for _, v := range arr { ... } // ignore the index
for r := range chanVar { ... } // receive until channel closes
break exits the innermost loop. continue jumps to the next iteration.
Labeled break / continue
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j > 6 {
break outer
}
}
}
Loop variable capture (post Go 1.22 fix)
Before Go 1.22 the loop variable was shared across iterations, which broke goroutines and closures. Since Go 1.22 each iteration gets a fresh variable, so this works as you'd expect:
for i := 0; i < 3; i++ {
go func() { fmt.Println(i) }() // 0, 1, 2 (in some order) on Go 1.22+
}
For older Go you had to write i := i inside the loop.
switch
Go's switch doesn't fall through by default. Each case is independent.
switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
fmt.Println("weekday")
case "Sat", "Sun":
fmt.Println("weekend")
default:
fmt.Println("?")
}
You can omit the expression for a clean if/else chain:
switch {
case x < 0:
sign = -1
case x == 0:
sign = 0
default:
sign = 1
}
fallthrough forces execution into the next case (rarely used):
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("one or two")
}
Type switch (preview, see Chapter 7)
switch v := x.(type) {
case int:
...
case string:
...
default:
...
}
defer
defer schedules a call to run when the surrounding function returns
(normally or via panic). Deferred calls run in LIFO order.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // guaranteed to run on every return path
// ... use f ...
return nil
}
Important rules:
- Arguments to a deferred call are evaluated immediately, but the call itself runs at function return:
go
i := 1
defer fmt.Println("i =", i) // prints "i = 1"
i = 99
- Multiple defers run in LIFO:
go
defer fmt.Println("A")
defer fmt.Println("B")
defer fmt.Println("C")
// prints C, B, A
goto
Exists for unusual control flow inside a single function, but is rarely seen in idiomatic code (mostly used in generated parsers).
panic and recover (preview)
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("boom")
}
panic should be reserved for truly unrecoverable bugs. Use error returns
for normal failures (Chapter 7).
Run the example
go run ./part1-language/examples/05_control