Skip to content
gohacks.dev
Basics

Switch in Go and Why There Is No Implicit Fallthrough

Go switch cases stop on their own, take several values and need no expression. Learn fallthrough, the init statement, and the break trap inside a loop.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

A switch in C is a trap with a good idea inside: every case falls into the next unless you remember a break, and the one time you forget it, the bug ships. Go kept the construct and turned the default around. Each case stops on its own, and falling through is something you ask for in writing. Along with that come a few conveniences that make switch the right tool for more than integer dispatch. This how-to covers the forms you will write, the one trap that remains (a break that does not do what C taught you), and the places to prefer something else. It builds on functions and is part of the Go basics path.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The basic form

A switch compares an expression with each case in order and runs the first that equals it. A case can list several values:

main.goGo
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() {
	for _, d := range []string{"sat", "wed", "xyz"} {
		fmt.Println(d, kind(d))
	}
}
Text
sat weekend
wed weekday
xyz unknown

There is no break anywhere, and each call returns after its case. The default branch runs when nothing matches, and it may appear anywhere in the list, though the end is where readers look. Cases are tested from top to bottom, and the first match wins, so order matters if values overlap. Constants that repeat are caught at compile time:

main.goGo
package main

import "fmt"

func main() {
	n := 2
	switch n {
	case 1:
		fmt.Println("one")
	case 2:
		fmt.Println("two")
	case 2:
		fmt.Println("also two")
	}
}
Text
# example.test/lab
./main.go:12:7: duplicate case 2 (constant of type int) in expression switch
	./main.go:10:7: previous case

The compiler rejects the duplicate case 2. It is one of the small ways a Go switch prevents the copy-and-paste mistake in a long list of values. The check applies to integer, string and other constant cases; it does not apply to cases that are computed at run time.

A switch with no expression

Omit the expression and each case becomes a boolean condition. It is the neat replacement for a chain of if, else if:

main.goGo
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"
	default:
		return "F"
	}
}

func main() {
	for _, s := range []int{95, 85, 72, 40} {
		fmt.Print(grade(s), " ")
	}
	fmt.Println()
}
Text
A B C F

Order is the logic here: 95 satisfies the first three conditions and gets A because the first match wins. If you flip the order you get a different grade for the same input, so the sequence is part of the meaning. A switch with conditions reads better than four else if lines once there are three or more branches, and I stop at two.

An init statement and scoping

A switch can start with a short statement, like if, whose variables live in the whole switch:

main.goGo
package main

import (
	"fmt"
	"strconv"
)

func classify(s string) string {
	switch n, err := strconv.Atoi(s); {
	case err != nil:
		return "not a number"
	case n < 0:
		return "negative"
	case n == 0:
		return "zero"
	default:
		return fmt.Sprintf("positive %d", n)
	}
}

func main() {
	for _, s := range []string{"12", "-4", "0", "x"} {
		fmt.Println(s, "->", classify(s))
	}
}
Text
12 -> positive 12
-4 -> negative
0 -> zero
x -> not a number

The n, err := ...; in the header is scoped to the switch, so neither name leaks into the rest of the function. The trailing semicolon followed by no expression gives you the tagless form. This is handy for parse-then-branch code, and it keeps err from colliding with another err in the function, which is the shadowing risk in a different place.

fallthrough, when you really mean it

The fallthrough statement passes control into the next case's body without testing that case. It must be the last statement in a case, and it cannot be used in the final case:

main.goGo
package main

import "fmt"

func perks(level int) []string {
	var out []string
	switch level {
	case 3:
		out = append(out, "priority support")
		fallthrough
	case 2:
		out = append(out, "extended storage")
		fallthrough
	case 1:
		out = append(out, "basic access")
	}
	return out
}

func main() {
	fmt.Println(perks(3))
	fmt.Println(perks(1))
}
Text
[priority support extended storage basic access]
[basic access]

Level 3 gets every perk because each case falls into the next, and level 1 gets only its own. It is a legitimate use, a cumulative ladder, and I find it rare. Most of the time a list of values in one case, or a switch with conditions, says the same thing more plainly. When a reviewer sees fallthrough, they will check that it is intentional, so a comment helps.

The break trap inside a loop

In C, break inside a switch leaves the switch, and in Go that is still true, which surprises people who expect it to leave the loop around it:

main.goGo
package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		switch {
		case i == 2:
			break
		default:
			fmt.Print(i, " ")
		}
	}
	fmt.Println()

search:
	for i := 0; i < 5; i++ {
		switch {
		case i == 2:
			break search
		default:
			fmt.Print(i, " ")
		}
	}
	fmt.Println()
}
Text
0 1 3 4 
0 1

The first loop prints 0 1 3 4. The break skipped printing 2 and the loop carried on. The second uses a label, break search, and stops entirely after 0 1. The label names the loop, and a break or continue with it can reach out from inside a switch, a select or a nested loop. The alternative is to move the loop into a function and return, which is often cleaner. The same trap exists for select statements, where the same label rule applies, and labels in nested loops shows the full set of cases.

A complete switch still needs a return

The compiler does not prove that your cases cover every input, so a function that returns from every case still needs something after the switch, unless there is a default that returns or panics too. Without it you get a compile error that surprises people who have just written an exhaustive-looking switch:

main.goGo
package main

import "fmt"

type Level int

const (
	Low Level = iota
	High
)

func label(l Level) string {
	switch l {
	case Low:
		return "low"
	case High:
		return "high"
	}
}

func main() {
	fmt.Println(label(High))
}
Text
# example.test/lab
./main.go:19:1: missing return

The error is missing return, and it is correct: Level(7) is a legal Level that matches neither case. The fix is a default branch. For an enum where an unknown value is a programming error, default: panic(fmt.Sprintf("unknown level %d", l)) makes the failure loud, and for a value that can legitimately arrive from outside, return an error or a placeholder. The same message appears in other shapes, and missing return at the end of a function lists them.

Switching on types

A switch can also branch on the dynamic type of an interface value, with switch v := x.(type). That form has its own rules and its own article, type assertions and type switches, and the structure above carries over. The case nil branch, the multi-type case and the variable's type inside each case are what differ.

When another construct fits better

If every case maps a key to a value, such as an error code to a message, a map literal is shorter and can be built at run time. If every case calls a different function with the same arguments, a map from key to function removes the switch, and the dispatch table can grow from several places. A switch over a closed set of values, such as an enum made with iota, is the right shape, and there the missing case is a bug that a linter can find: the exhaustive analyzer flags a switch that forgets an enum value. For two branches, a plain if is clearer. The language specification on switch statements lists the expression and type forms, and Effective Go on switch shows the tagless and comma-separated forms with commentary.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.