Switch Statements
What is a Switch Statement?β
A switch statement selects one of many code paths to execute. In most languages, switch is a thin wrapper around a sequence of if-else if comparisons with some syntactic sugar. Go goes further: its switch is more expressive, safer, and more readable than the C-family original.
Three properties make Go's switch stand out:
- No automatic fallthrough. Each
caseblock is self-contained. You never need abreakto prevent falling into the next case β that is the default behavior. Explicit fallthrough requires thefallthroughkeyword. - Conditionless switch. You can omit the expression after
switchentirely. Go then evaluates eachcaseas a boolean expression, making it a cleaner alternative to a longif-else ifchain. - Type switch. A special form of
switchthat interrogates the dynamic type of an interface value. This is the idiomatic way to implement type-based dispatch in Go.
Expression Switchβ
The most common form: compare a value against a list of cases.
package main
import "fmt"
func dayType(day string) string {
switch day {
case "Saturday", "Sunday":
return "Weekend"
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
return "Weekday"
default:
return "Unknown"
}
}
func main() {
days := []string{"Monday", "Saturday", "Sunday", "Friday", "Holiday"}
for _, d := range days {
fmt.Printf("%s β %s\n", d, dayType(d))
}
}
Multiple values per case are separated by commas β no need for separate case lines.
Conditionless Switchβ
When you omit the expression, each case is evaluated as a standalone boolean condition. This replaces a long if-else if chain with something more readable:
package main
import "fmt"
func classify(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 70:
return "C"
case score >= 60:
return "D"
default:
return "F"
}
}
func main() {
scores := []int{95, 83, 71, 65, 42}
for _, s := range scores {
fmt.Printf("Score %d β Grade %s\n", s, classify(s))
}
}
Switch with Initialization Statementβ
Just like if, a switch can include an initialization statement:
package main
import (
"fmt"
"time"
)
func main() {
switch hour := time.Now().Hour(); {
case hour < 6:
fmt.Println("Late night")
case hour < 12:
fmt.Println("Morning")
case hour < 18:
fmt.Println("Afternoon")
default:
fmt.Println("Evening")
}
}
Type Switchβ
A type switch inspects the dynamic (runtime) type of an interface value. The syntax uses x.(type) β which is only valid inside a switch statement.
package main
import "fmt"
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("integer: %d (hex: %#x)", v, v)
case float64:
return fmt.Sprintf("float64: %f", v)
case string:
return fmt.Sprintf("string: %q (len=%d)", v, len(v))
case bool:
return fmt.Sprintf("bool: %t", v)
case []int:
return fmt.Sprintf("[]int with %d elements: %v", len(v), v)
case nil:
return "nil value"
default:
return fmt.Sprintf("unknown type: %T", v)
}
}
func main() {
values := []interface{}{
42,
3.14,
"hello",
true,
[]int{1, 2, 3},
nil,
struct{ Name string }{"Go"},
}
for _, v := range values {
fmt.Println(describe(v))
}
}
Inside each case, v is already asserted to that concrete type β you can call type-specific methods without an explicit type assertion.
With any (Go 1.18+):interface{} and any are identical. You will see both in modern Go codebases.
fallthrough Keywordβ
In Go, cases do not fall through by default. The fallthrough keyword forces execution to continue into the next case body β even if that case's condition is false.
package main
import "fmt"
func main() {
n := 2
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
fallthrough
case 3:
fmt.Println("three")
case 4:
fmt.Println("four")
}
// Output:
// two
// three
}
Important gotcha:fallthrough transfers control unconditionally to the next case's body β the next case's condition is not evaluated. This is rarely what you want. The idiomatic Go alternative is to list multiple values in a single case: case 1, 2, 3:.
Practical Example: HTTP Method Routerβ
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
var products = []Product{
{1, "Go Programming Book", 39.99},
{2, "Mechanical Keyboard", 129.99},
}
func productsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
if err := json.NewEncoder(w).Encode(products); err != nil {
http.Error(w, "encoding error", http.StatusInternalServerError)
}
case http.MethodPost:
var p Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
p.ID = len(products) + 1
products = append(products, p)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(p)
case http.MethodDelete:
products = nil
w.WriteHeader(http.StatusNoContent)
case http.MethodOptions:
w.Header().Set("Allow", "GET, POST, DELETE, OPTIONS")
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w,
fmt.Sprintf("method %s not allowed", r.Method),
http.StatusMethodNotAllowed,
)
}
}
func main() {
http.HandleFunc("/products", productsHandler)
fmt.Println("Server listening on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
fmt.Println("Server error:", err)
}
}
Practical Example: Interface Type Dispatchingβ
Type switches shine when implementing logic that must handle multiple concrete types that share an interface.
package main
import (
"fmt"
"math"
"strings"
)
// Shape is the shared interface
type Shape interface {
Area() float64
Perimeter() float64
}
type Circle struct {
Radius float64
}
type Rectangle struct {
Width, Height float64
}
type Triangle struct {
A, B, C float64 // side lengths
}
func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius }
func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.Radius }
func (r Rectangle) Area() float64 { return r.Width * r.Height }
func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) }
func (t Triangle) Area() float64 {
s := (t.A + t.B + t.C) / 2
return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
// describeShape uses a type switch for shape-specific information
func describeShape(s Shape) {
var detail string
switch v := s.(type) {
case Circle:
detail = fmt.Sprintf("Circle(r=%.2f)", v.Radius)
case Rectangle:
detail = fmt.Sprintf("Rectangle(%.2f x %.2f)", v.Width, v.Height)
case Triangle:
sides := fmt.Sprintf("%.2f, %.2f, %.2f", v.A, v.B, v.C)
detail = fmt.Sprintf("Triangle(sides=%s)", sides)
default:
detail = fmt.Sprintf("Unknown(%T)", v)
}
fmt.Printf("%-35s area=%-10.4f perimeter=%.4f\n",
detail, s.Area(), s.Perimeter())
}
func main() {
fmt.Println(strings.Repeat("-", 70))
shapes := []Shape{
Circle{Radius: 5},
Rectangle{Width: 4, Height: 6},
Triangle{A: 3, B: 4, C: 5},
Circle{Radius: 1},
}
for _, s := range shapes {
describeShape(s)
}
fmt.Println(strings.Repeat("-", 70))
}
Expert Tipsβ
Prefer case a, b, c: over fallthrough. Listing multiple values in one case is clearer, safer, and the idiomatic Go choice. Reserve fallthrough for the rare cases where truly unconditional cascade is intended β and always leave a comment explaining why.
Type switches are exhaustive documentation. Seeing all cases listed in a type switch tells future readers exactly which types a function can receive. If you add a new concrete type, the compiler will not warn you about a missing case β but adding a default that panics (default: panic(fmt.Sprintf("unhandled type %T", v))) catches the omission at runtime during testing.
switch over constants uses the compiler's jump table. For dense integer ranges, the Go compiler can emit a jump table instead of a linear comparison chain, making switch significantly faster than an equivalent if-else if chain.
Avoid side effects in switch expressions. The switch expression is evaluated once. If it has side effects (e.g., a function call that advances a cursor), those side effects happen exactly once β not once per case.