Skip to main content

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:

  1. No automatic fallthrough. Each case block is self-contained. You never need a break to prevent falling into the next case — that is the default behavior. Explicit fallthrough requires the fallthrough keyword.
  2. Conditionless switch. You can omit the expression after switch entirely. Go then evaluates each case as a boolean expression, making it a cleaner alternative to a long if-else if chain.
  3. Type switch. A special form of switch that 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.