The problem with long if chains
You are writing a command parser for a CLI tool. It starts simple: start, stop, restart. You write an if chain. It works. Then you add debug, verbose, quiet, status, version. The chain grows. Indentation creeps right. You copy-paste a case and forget to update the string literal. In languages like C or Java, you might forget a break and accidentally execute the next block. Go's switch statement exists to keep branching logic tight, readable, and safe from fall-through bugs.
Switch as a dispatcher
Think of a switch like a mailroom sorter. You hand it a letter with a destination stamp. The sorter reads the stamp once and slides the letter down the correct chute. It does not check every chute manually. It stops at the first match. If no chute matches, the letter goes to the default bin.
Go's switch evaluates the expression once. It checks cases in order. When a match is found, the corresponding block runs. Execution exits the switch immediately. There is no implicit fall-through. If you want to run the next case, you must write fallthrough explicitly. This design prevents the "missing break" bug that plagues many other languages.
Basic syntax
Here's the simplest switch: evaluate a variable, match cases, handle the rest with default.
package main
import "fmt"
func main() {
status := "active"
// switch evaluates status once and compares against each case
switch status {
case "active":
// case matches exact string value
fmt.Println("User is active")
case "inactive":
// execution stops here if matched
fmt.Println("User is inactive")
default:
// default catches any value not listed above
fmt.Println("Unknown status")
}
}
Cases can match multiple values by separating them with commas. The compiler treats this as a single case block.
switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
// comma separates multiple values for the same block
fmt.Println("Weekday")
case "Sat", "Sun":
fmt.Println("Weekend")
}
How the compiler handles it
The compiler checks that the tag expression and all case values are comparable. You cannot switch on slices, maps, or functions. You can switch on integers, strings, booleans, structs with comparable fields, and interfaces.
Case values must be constant expressions. You cannot use a variable or a function call in a case clause. The compiler rejects code like case len(s) > 5: with case clause must be a constant. This restriction allows the compiler to optimize the switch into a jump table or binary search for large sets of constants.
The gofmt tool aligns case clauses vertically. This alignment is automatic. Do not try to manually align cases with extra spaces; gofmt will reset them. Trust the formatter to keep the structure readable.
Switch without an expression
When you need to check multiple boolean conditions, you can omit the expression after switch. This is equivalent to switch true. Each case is a boolean expression. The first case that evaluates to true runs.
Use this pattern when you have range checks or complex conditions that don't fit a simple value match. It keeps the indentation cleaner than a nested if/else chain.
Here's a switch without an expression for grading logic:
package main
import "fmt"
func main() {
score := 85
// switch without expression evaluates each case as a boolean
switch {
case score >= 90:
// first true case wins
fmt.Println("Grade: A")
case score >= 80:
// checked only if previous case was false
fmt.Println("Grade: B")
case score >= 70:
fmt.Println("Grade: C")
default:
fmt.Println("Grade: F")
}
}
The order matters. The compiler checks cases top to bottom. If you put case score >= 70: before case score >= 80:, the higher score will match the lower threshold and produce the wrong result. Always order conditions from most specific to least specific, or ensure they are mutually exclusive.
Type switches for interfaces
Switches shine when you need to handle different concrete types behind an interface. A type switch uses the syntax x.(type) inside the case clause. You can also declare a short variable to extract the value with its concrete type.
This pattern is common in serialization, logging, or when wrapping third-party interfaces.
Here's a type switch that handles different numeric types:
package main
import "fmt"
func describe(val interface{}) {
// type switch checks the dynamic type of the interface value
switch v := val.(type) {
case int:
// v has type int inside this case
fmt.Printf("Integer: %d\n", v)
case float64:
// v has type float64 inside this case
fmt.Printf("Float: %f\n", v)
case string:
// v has type string inside this case
fmt.Printf("String: %s\n", v)
default:
// v is still interface{} in default
fmt.Printf("Unknown type: %T\n", v)
}
}
func main() {
describe(42)
describe(3.14)
describe("hello")
}
The variable v is scoped to each case block. Its type changes based on the matched case. In the default block, v retains the original interface type. If you do not need the value, use the blank identifier to discard it. Write case int: without a variable declaration. The compiler understands that you are ignoring the concrete value.
Pitfalls and compiler errors
New Go developers often expect fall-through behavior from other languages. Go does not fall through by default. If you write code assuming fall-through, the logic will skip cases silently. The fallthrough keyword forces execution into the next case, but it is rarely needed. Use it only when you have a compelling reason, such as grouping cases that share a small amount of logic.
The compiler enforces strict rules on fallthrough. You cannot use it in the last case or in a case with multiple values. The compiler rejects invalid usage with fallthrough statement out of place.
Duplicate case values are forbidden. If you list the same value in two cases, the compiler stops with duplicate case value "active" in switch. This error catches copy-paste mistakes early.
Type mismatches between the tag and cases also fail. If you switch on an integer but use a string in a case, the compiler complains with cannot use "active" (untyped string constant) as int value in case clause. Ensure all case values match the type of the switch expression, or use a type switch for interfaces.
Another common mistake is using a switch on a struct that contains a slice or map. Structs are comparable only if all their fields are comparable. The compiler rejects switches on non-comparable types with invalid operation: switch on struct containing slice. Convert the struct to a comparable key or switch on a specific field instead.
Conventions and style
The Go community follows a few conventions around switches. Always include a default case unless you can prove that all possible values are covered. For enums or bit flags, a missing default might hide a bug when a new value is added. The default case acts as a safety net.
When using a type switch, prefer the short variable declaration v := x.(type) over checking the type and then asserting again. The declaration gives you the typed value directly. Avoid double assertions like case int: val := x.(int). The compiler already knows the type inside the case.
Use the blank identifier _ to discard values you do not need. In a type switch, case int: says you care about the type but not the value. In a regular switch, you rarely need _ because the tag is already available.
Switch initialization is a lesser-known feature. You can add a statement before the tag expression: switch val := compute(); val { ... }. The variable val is scoped to the switch block. This keeps temporary variables out of the outer scope. Use it when the computation is expensive or when you want to limit the variable's lifetime.
Decision matrix
Use a switch with an expression when you are matching a single value against a list of constants. Use a switch without an expression when you need to evaluate multiple independent boolean conditions and want cleaner indentation than a nested if chain. Use a type switch when you have an interface value and need to dispatch based on its underlying concrete type. Use a plain if/else chain when you have only two branches or when the conditions involve complex logic that does not fit a case structure. Use a map lookup when you have a large set of static mappings and want O(1) dispatch without verbose case clauses.
Switch is a dispatcher, not a loop. Fallthrough is a trap. Use it only when you know exactly why. Trust gofmt to align your cases. Focus on the logic.