Polyglot
Languages Go pattern matching
Go § pattern-matching

Switch and pattern dispatch

Go does not have pattern matching in the sense of Rust, Haskell, or Scala — there is no destructuring, no algebraic-data-type analysis, and no exhaustive checking. The conventional dispatch construct is switch, which has two principal forms: expression switch (matching on a value) and type switch (matching on the dynamic type of an interface). Each switch arm runs to completion (no implicit fallthrough — the C-family default), the arms are checked in order, and the construct is a statement rather than an expression. The combination — switch with implicit break, the type-switch form for interface dispatch, the init clause shared with if and for — is the substance of Go’s value-driven dispatch.

Expression switch

The principal form:

switch n {
case 0:
    fmt.Println("zero")
case 1:
    fmt.Println("one")
case 2, 3, 4:                                    // multiple values per case
    fmt.Println("low")
case 5, 6, 7, 8, 9:
    fmt.Println("high")
default:
    fmt.Println("other")
}

The discriminant follows switch; each case lists one or more values; default matches anything not covered. The cases are tried in order; the first matching case’s body runs; no fallthrough — the body runs to completion and exits the switch.

No fallthrough by default

Unlike C-family switch, Go’s cases do not fall through. The body of each case runs once and the switch exits:

switch n {
case 1:
    fmt.Println("one")
    // implicit break — no fallthrough
case 2:
    fmt.Println("two")
}

To explicitly fall through, use the fallthrough keyword:

switch n {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two")                            // also runs if n == 1
}

The fallthrough is rarely used; conventional Go style avoids it. The default no-fallthrough behaviour eliminates a substantial class of bugs.

Conditionless switch

Go admits switch without a discriminant — equivalent to if/else if/else:

switch {
case n < 0:
    fmt.Println("negative")
case n == 0:
    fmt.Println("zero")
case n < 10:
    fmt.Println("small")
default:
    fmt.Println("large")
}

The form is conventionally clearer than long if/else if chains for value-driven branching.

Init clause

switch admits an initialisation clause, like if:

switch n := compute(); {                          // empty discriminant; no value
case n < 0:
    return "negative"
case n == 0:
    return "zero"
default:
    return "positive"
}

switch n := compute(); n {                        // discriminant after init
case 0, 1:
    return "low"
default:
    return "other"
}

The variable n is scoped to the switch and its cases.

Type switch

A type switch dispatches on the dynamic type of an interface value:

var i interface{} = "hello"

switch v := i.(type) {
case nil:
    fmt.Println("nil")
case int:
    fmt.Printf("int: %d\n", v)
case string:
    fmt.Printf("string: %s\n", v)
case []byte:
    fmt.Printf("bytes: %x\n", v)
case error:
    fmt.Printf("error: %v\n", v)
default:
    fmt.Printf("unknown type %T: %v\n", v, v)
}

The form switch v := i.(type) is special syntax — (type) is recognised only in switch. The variable v is bound in each case to the matched type.

The mechanism admits substantial flexibility for handling values whose static type is interface{} or some other interface. Conventional uses include JSON deserialisation, error type discrimination, and value-driven processing of heterogeneous data.

For a single-type extraction, use a type assertion:

if s, ok := i.(string); ok {
    fmt.Println(s)
}

Multiple values per case

Each case may list multiple values, separated by commas:

switch day {
case "Saturday", "Sunday":
    fmt.Println("weekend")
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    fmt.Println("weekday")
}

The mechanism admits compact dispatch for sets of values.

Type-switch with multiple types per case

Type switch admits multiple types per case:

switch v := i.(type) {
case int, int64:
    fmt.Printf("integer: %d\n", v)               // v's type is interface{} (the common type)
case float32, float64:
    fmt.Printf("float: %f\n", v)
case string:
    fmt.Printf("string: %s\n", v)
}

A subtlety: when a case has multiple types, the variable v retains the interface type — the compiler cannot narrow to a specific one of the listed types.

Common patterns

State machine

switch state {
case StateIdle:
    return startProcessing(data)
case StateRunning:
    return continueProcessing(data)
case StateDone:
    return finalise(data)
case StateFailed:
    return fmt.Errorf("processing already failed")
}

Error type dispatch

err := operation()
switch e := err.(type) {
case nil:
    return nil
case *NetworkError:
    return retryAfter(e.RetryAfter)
case *ValidationError:
    return fmt.Errorf("validation: %s", e.Field)
case *AuthError:
    return ErrUnauthorized
default:
    return fmt.Errorf("unknown: %w", err)
}

The pattern is conventional for Go’s typed errors; errors.As (Go 1.13+) admits a more flexible form for wrapped errors:

var netErr *NetworkError
if errors.As(err, &netErr) {
    return retryAfter(netErr.RetryAfter)
}

JSON value dispatch

func formatValue(v interface{}) string {
    switch x := v.(type) {
    case nil:
        return "null"
    case bool:
        if x { return "true" }
        return "false"
    case float64:
        return strconv.FormatFloat(x, 'f', -1, 64)
    case string:
        return strconv.Quote(x)
    case []interface{}:
        return formatArray(x)
    case map[string]interface{}:
        return formatObject(x)
    default:
        return fmt.Sprintf("%v", x)
    }
}

The pattern is conventional for processing encoding/json values.

Range-based dispatch

switch {
case n < 0:
    return "negative"
case n == 0:
    return "zero"
case n < 10:
    return "small"
case n < 100:
    return "medium"
default:
    return "large"
}

The conditionless switch is conventional for range-driven dispatch.

Integer-to-string lookup

func dayName(n int) string {
    switch n {
    case 1: return "Monday"
    case 2: return "Tuesday"
    case 3: return "Wednesday"
    case 4: return "Thursday"
    case 5: return "Friday"
    case 6: return "Saturday"
    case 7: return "Sunday"
    default: return "invalid"
    }
}

For substantially larger lookups, a map is conventionally clearer:

var dayNames = map[int]string{
    1: "Monday", 2: "Tuesday", 3: "Wednesday",
    4: "Thursday", 5: "Friday", 6: "Saturday", 7: "Sunday",
}

func dayName(n int) string {
    if name, ok := dayNames[n]; ok {
        return name
    }
    return "invalid"
}

Token-based dispatch (parser)

switch tok.Kind {
case TokInt:
    return parseInt(tok)
case TokString:
    return parseString(tok)
case TokOpenParen:
    return parseGroup()
case TokIdent:
    return parseIdent(tok)
default:
    return fmt.Errorf("unexpected token: %v", tok)
}

HTTP method dispatch

switch r.Method {
case http.MethodGet:
    return handleGet(w, r)
case http.MethodPost:
    return handlePost(w, r)
case http.MethodDelete:
    return handleDelete(w, r)
default:
    http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
    return nil
}

Optional cleanup

switch err := compute(); {
case err == nil:
    return result
case errors.Is(err, ErrTransient):
    return retryLater()
case errors.Is(err, ErrFatal):
    return abort(err)
default:
    log.Printf("unexpected error: %v", err)
    return err
}

Type-switch with default

switch v := value.(type) {
case int:
    return fmt.Sprintf("int %d", v)
case string:
    return fmt.Sprintf("string %q", v)
default:
    return fmt.Sprintf("unknown type %T", v)
}

The default is conventional in type switches for handling unexpected types.

A note on the absence of pattern matching

Languages with substantial pattern matching (Rust, Haskell, Scala) admit:

  • Destructuringcase (x, y, z) =>.
  • Bindings within patternscase Some(x) if x > 0 =>.
  • Algebraic data type analysis — exhaustive checking on enums.
  • Arbitrary nested patternscase (Cons(x, xs), Cons(y, ys)) =>.

Go’s switch is more limited — it dispatches on values or types but does not destructure. The conventional Go style decomposes pattern-like operations into:

  • A switch for the principal dispatch.
  • A type assertion or struct field access for the “destructuring”.
// Pattern-matching style (e.g., Rust):
// match value {
//     Point { x: 0, y } => println!("y-axis at {}", y),
//     Point { x, y: 0 } => println!("x-axis at {}", x),
//     Point { x, y } => println!("({}, {})", x, y),
// }

// Go equivalent:
switch {
case p.X == 0 && p.Y == 0:
    fmt.Println("origin")
case p.X == 0:
    fmt.Printf("y-axis at %d\n", p.Y)
case p.Y == 0:
    fmt.Printf("x-axis at %d\n", p.X)
default:
    fmt.Printf("(%d, %d)\n", p.X, p.Y)
}

The conventional Go style is more verbose but admits less syntactic surface to learn.

A note on exhaustiveness

Go’s switch does not check exhaustiveness — adding a new value to an enum-like type does not produce errors at every switch. The conventional defences:

  • Always include a default case for safety.
  • Use a linter (go vet, staticcheck, exhaustive) — exhaustive checks switch on enum types.
  • Use panic("unreachable") in cases that should never occur.
const (
    StatusActive Status = iota
    StatusInactive
    StatusBanned
)

switch s {
case StatusActive:
    return "active"
case StatusInactive:
    return "inactive"
case StatusBanned:
    return "banned"
default:
    panic(fmt.Sprintf("unknown status: %d", s))
}

A note on the conventional discipline

The contemporary Go switch advice:

  • Use switch for value or type dispatch.
  • Use conditionless switch (switch { case ... }) in place of long if/else if chains.
  • Use type switches for interface-based dispatch.
  • Avoid fallthrough — the default no-fallthrough is the right behaviour.
  • Always include default — defensive against new variants.
  • Use errors.Is and errors.As (Go 1.13+) for wrapped error matching.
  • Use a map for substantially larger value-to-result lookups.

The combination — single dispatch construct (switch), expression and type forms, no fallthrough by default, init clause, multiple values per case — is the substance of Go’s pattern-dispatch surface. The discipline trades pattern-matching expressiveness for simplicity and explicitness.