Go · lesson 6 of 25
Control Flow in Go
Go control flow run for real: if with an init statement, switch without fallthrough, the single for loop, range, and labelled break and continue.
By SkillByExample · Updated
Go has if, switch, and exactly one loop keyword, for. There are no parentheses around conditions and braces are always required. Each example is a complete program.
if, else, and the init statement
An if can start with a short statement whose variables live only inside the if/else chain.
package main
import (
"fmt"
"strconv"
)
func main() {
for _, in := range []string{"42", "-3", "x"} {
if n, err := strconv.Atoi(in); err != nil {
fmt.Println(in, "-> not a number")
} else if n < 0 {
fmt.Println(in, "-> negative")
} else {
fmt.Println(in, "-> ok:", n)
}
}
}
$ go run ifinit.go
42 -> ok: 42
-3 -> negative
x -> not a number
n and err are visible in every branch of the chain but not after it, which keeps an error variable from leaking into the rest of the function. This if v, err := ...; err != nil shape is the most common line in Go code.
The condition must be a bool and the braces are mandatory — even a one-line body needs them:
package main
import "fmt"
func main() {
x := 3
if x > 2 fmt.Println("big")
}
$ go run ifnobrace.go
# command-line-arguments
./ifnobrace.go:7:11: syntax error: unexpected name fmt, expected {
./ifnobrace.go:8:1: syntax error: unexpected }, expected expression
switch
Cases are tested top to bottom and each one ends by itself — there is no break to forget.
package main
import "fmt"
func kind(day string) string {
switch day {
case "Sat", "Sun":
return "weekend"
case "Mon", "Tue", "Wed", "Thu", "Fri":
return "weekday"
default:
return "unknown"
}
}
func main() {
fmt.Println(kind("Sun"), kind("Wed"), kind("Funday"))
}
$ go run switch.go
weekend weekday unknown
A comma-separated list replaces C’s stacked fall-through cases. A switch with no expression is a cleaner if/else if chain, where each case is a boolean:
package main
import "fmt"
func grade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
}
return "F"
}
func main() {
for _, s := range []int{95, 80, 79, 12} {
fmt.Print(grade(s), " ")
}
fmt.Println()
}
$ go run switchtrue.go
A B C F
The first true case wins, so 80 is a B even though score >= 70 is also true.
fallthrough doesn’t test the next case
fallthrough exists, but it runs the next case’s body unconditionally.
package main
import "fmt"
func main() {
n := 5
switch {
case n > 3:
fmt.Println("greater than 3")
fallthrough
case n > 100:
fmt.Println("greater than 100?!")
case n > 0:
fmt.Println("positive")
}
}
$ go run fallthrough.go
greater than 3
greater than 100?!
5 is not greater than 100, yet that body ran: fallthrough skips the condition entirely. It also stops there — positive never prints, because only one case matched and the fall went one step. This is why fallthrough is rare; a comma list or a helper function is almost always clearer.
for: the only loop
The three-part form, the condition-only form (“while”), and the bare form (infinite) are all for.
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
fmt.Print(i, " ")
}
fmt.Println("| classic")
n := 1
for n < 100 {
n *= 3
}
fmt.Println(n, "| while-style")
tries := 0
for {
tries++
if tries == 4 {
break
}
}
fmt.Println(tries, "| infinite + break")
}
$ go run loops.go
0 1 2 | classic
243 | while-style
4 | infinite + break
The while-style loop stops at 243 — the first power of three not below 100, because the condition is checked before each pass, not after. for {} runs until a break, return or panic.
range
range iterates over integers (Go 1.22+), slices, strings, maps and channels.
package main
import "fmt"
func main() {
for i := range 3 {
fmt.Print(i, " ")
}
fmt.Println()
for i, v := range []string{"a", "b", "c"} {
fmt.Print(i, "=", v, " ")
}
fmt.Println()
for i, r := range "héllo" {
fmt.Print(i, ":", string(r), " ")
}
fmt.Println()
}
$ go run rangeall.go
0 1 2
0=a 1=b 2=c
0:h 1:é 3:l 4:l 5:o
range 3 counts 0, 1, 2. Over a slice you get index and element. Over a string you get byte offsets and decoded runes: é is two bytes, so the index jumps from 1 to 3.
range gives you a copy of each element, so assigning to it changes nothing:
package main
import "fmt"
func main() {
prices := []int{10, 20, 30}
for _, p := range prices {
p *= 2
}
fmt.Println(prices)
for i := range prices {
prices[i] *= 2
}
fmt.Println(prices)
}
$ go run rangecopy.go
[10 20 30]
[20 40 60]
The first loop doubled a local copy each time and threw it away. To modify the slice, index into it.
Maps are the one place range order is not fixed — Go randomises it on purpose:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
stock := map[string]int{"apple": 5, "kiwi": 2, "fig": 9, "plum": 1}
for k := range stock {
fmt.Print(k, " ")
}
fmt.Println("<- map order")
for _, k := range slices.Sorted(maps.Keys(stock)) {
fmt.Print(k, "=", stock[k], " ")
}
fmt.Println("<- sorted")
}
$ go run rangemap.go
apple kiwi fig plum <- map order
apple=5 fig=9 kiwi=2 plum=1 <- sorted
$ go run rangemap.go
apple kiwi fig plum <- map order
apple=5 fig=9 kiwi=2 plum=1 <- sorted
$ go run rangemap.go
fig plum apple kiwi <- map order
apple=5 fig=9 kiwi=2 plum=1 <- sorted
Three runs of the same program: the sorted line never changes, the map-order line does — and a run can repeat an earlier order by chance, so never treat one matching run as proof of stability. Sorting the keys (maps.Keys plus slices.Sorted, Go 1.23+) gives a stable order whenever output or tests depend on it.
break, continue, and labels
continue skips to the next iteration; break leaves the innermost loop. A label lets either target an outer loop.
package main
import "fmt"
func main() {
grid := [][]int{{1, 2, 3}, {4, -1, 6}, {7, 8, 9}}
search:
for r, row := range grid {
for c, v := range row {
if v%2 == 0 {
continue // skip even numbers
}
if v < 0 {
fmt.Println("negative at", r, c)
break search // leaves both loops
}
fmt.Print(v, " ")
}
}
fmt.Println("done")
}
$ go run labels.go
1 3 negative at 1 1
done
Without the label, break would only end the inner loop and the scan would carry on into the third row (7 9 would print). A plain break inside a switch or select also only leaves that statement, not the enclosing for — another case where a labelled break is needed.
Go also has goto, which jumps to a label in the same function. You will rarely see it outside generated code; a loop or an early return says the same thing more clearly.
Putting it together: FizzBuzz
package main
import "fmt"
func main() {
for i := range 15 {
n := i + 1
switch {
case n%15 == 0:
fmt.Print("FizzBuzz ")
case n%3 == 0:
fmt.Print("Fizz ")
case n%5 == 0:
fmt.Print("Buzz ")
default:
fmt.Print(n, " ")
}
}
fmt.Println()
}
$ go run fizzbuzz.go
1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
The n%15 case has to come first: switch cases are tested in order, and 15 would otherwise match n%3 == 0 and print Fizz.
Practice
1. Rewrite grade from switchtrue.go so that scores outside 0–100 return "invalid", and test it with 105 and -1.
Solution
package main
import "fmt"
func grade(score int) string {
switch {
case score < 0 || score > 100:
return "invalid"
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
}
return "F"
}
func main() {
for _, s := range []int{105, 90, 50, -1} {
fmt.Print(grade(s), " ")
}
fmt.Println()
}
$ go run p1.go
invalid A F invalid
The range check has to be the first case, since 105 would otherwise match score >= 90.
2. Use a while-style for to compute how many times 1000 can be halved (integer division) before it reaches 0.
Solution
package main
import "fmt"
func main() {
n, steps := 1000, 0
for n > 0 {
n /= 2
steps++
}
fmt.Println(steps)
}
$ go run p2.go
10
1000 → 500 → 250 → 125 → 62 → 31 → 15 → 7 → 3 → 1 → 0 is ten halvings — the bit length of 1000.
3. Modify labels.go so that, instead of stopping at the negative number, it skips the rest of that row and continues with the next row.
Solution
package main
import "fmt"
func main() {
grid := [][]int{{1, 2, 3}, {4, -1, 6}, {7, 8, 9}}
rows:
for _, row := range grid {
for _, v := range row {
if v < 0 {
continue rows
}
fmt.Print(v, " ")
}
}
fmt.Println()
}
$ go run p3.go
1 2 3 4 7 8 9
continue rows abandons the inner loop and starts the next outer iteration, so 6 is skipped but the third row prints.
4. Print each word of "go is fun" with its length using strings.Fields and range, numbering the lines from 1.
Solution
package main
import (
"fmt"
"strings"
)
func main() {
for i, w := range strings.Fields("go is fun") {
fmt.Println(i+1, w, len(w))
}
}
$ go run p4.go
1 go 2
2 is 2
3 fun 3
range indexes from 0, so add 1 for human numbering.