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.