Skip to main content
Go · lesson 6 of 25
Go beginner 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.

Frequently Asked Questions

Does Go have a while loop?
No. Go has a single loop keyword, for. Write for condition { } for a while loop and for { } for an infinite loop. The three-part for i := 0; i < n; i++ form and for range cover everything else.
Does switch in Go fall through by default?
No, the opposite of C. Each case ends automatically, so no break is needed. To list several values for one branch, separate them with commas in a single case. The fallthrough keyword exists but jumps into the next case body without testing its condition.
Can I declare a variable inside an if statement in Go?
Yes. An init statement before the condition, as in if err := f(); err != nil { }, declares a variable scoped to the if and its else branches. It keeps short-lived values such as errors out of the surrounding function.
Why is the order of a range loop over a map different each run?
Go deliberately randomises map iteration order so programs cannot come to depend on it. If you need a stable order, collect the keys, sort them with slices.Sort, and range over the sorted keys instead.
How do I break out of a nested loop in Go?
Put a label before the outer loop and use break label. A plain break only leaves the innermost loop, or the switch or select it is in. continue label likewise skips to the next iteration of the labelled loop.