Back to blog

Sunday, August 16, 2026

Go Type Assertion vs Type Switch: When to Use Each

Title card for 'Go Type Assertion vs Type Switch: When to Use Each' with a short code example

Use a type assertion when you care about one concrete type (or one extra interface). Use a type switch when you branch on several types. v := i.(T) panics if i is not T. v, ok := i.(T) does not. switch v := i.(type) is the multi-way form. It is not a conversion. It peeks at the pair already inside the interface.

The failure

This looks like a conversion. It is a crash waiting for a JSON number.

package main

import "fmt"

func asInt(v any) int {
    return v.(int) // panic if v is float64, json.Number, etc.
}

func main() {
    fmt.Println(asInt(3.0))
}

A chain of assertions is the other smell:

if n, ok := v.(int); ok {
    _ = n
} else if s, ok := v.(string); ok {
    _ = s
} else if b, ok := v.(bool); ok {
    _ = b
}

That is a type switch written the long way. Miss a type, forget ok, and you use a zero value as if the check succeeded.

Why they differ

An interface hides (dynamic type, dynamic value). An assertion asks: is the type exactly T (or does it satisfy interface U)? A type switch asks the same question several times, and in each case the bound variable has that case's static type.

var i any = "hi"
s := i.(string)           // panic on mismatch
s, ok := i.(string)       // ok == false, s == ""
switch v := i.(type) {
case string:
    // v is string
case int, int64:
    // v is still any: shared case
default:
    // v is still any
}

Asserting i.(Person) fails if the box holds *Person. Pointer and value are different dynamic types.

Asserting to another interface (err.(Temporary)) checks the method set, not a single struct. That is how optional capabilities work (io.Closer on an io.Reader).

A nil interface fails every concrete assertion. An interface that holds (*int)(nil) succeeds i.(*int): ok is true and the pointer is nil. That is the sibling nil-interface gotcha, not an assertion bug.

The fix

One type, untrusted input, comma-ok:

func asInt(v any) (int, bool) {
    n, ok := v.(int)
    return n, ok
}

One type, you just put it in the box, single-result is a programmer assertion: mismatch means your code is wrong.

Several types, type switch:

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int %d", x)
    case string:
        return fmt.Sprintf("string %q", x)
    case error:
        return x.Error()
    default:
        return fmt.Sprintf("%T", x)
    }
}

Optional extra methods, assertion to an interface:

func finish(r io.Reader) {
    if c, ok := r.(io.Closer); ok {
        _ = c.Close()
    }
}

Do not type-switch your way out of a missing interface. If every implementation needs Close, put Close on the interface you accept.