Skip to content

Type Assertions & Type Switches Intermediate

Introduction

Type assertions and type switches are Go's mechanisms for runtime type checking on interface values. Since interfaces in Go are satisfied implicitly, you sometimes need to discover or verify the concrete type behind an interface at runtime. Type assertions extract a concrete type from an interface value, while type switches branch on the dynamic type — similar to a regular switch but matching types instead of values. These are essential when working with any (empty interface), custom interface hierarchies, and standard library interfaces like io.Reader.


Syntax & Usage

Type Assertion: x.(T)

A type assertion extracts the concrete value of type T from an interface value x.

var i interface{} = "hello"

// Type assertion -- panics if wrong type
s := i.(string)
fmt.Println(s) // "hello"

// This would panic at runtime:
// n := i.(int) // panic: interface conversion: interface {} is string, not int

The Comma-Ok Pattern (Safe Assertion)

var i interface{} = "hello"

// Safe -- never panics
s, ok := i.(string)
if ok {
    fmt.Println("string:", s)
} else {
    fmt.Println("not a string")
}

// Common idiom: check and use in one step
if s, ok := i.(string); ok {
    fmt.Printf("Got string of length %d\n", len(s))
}

// When assertion fails: ok=false, value is zero value of T
n, ok := i.(int) // ok=false, n=0

Always Use Comma-Ok for Untrusted Values

A bare type assertion x.(T) panics if the interface doesn't hold type T. Always use the comma-ok form when the type isn't guaranteed.


Type Assertion on Named Interfaces

Type assertions aren't limited to interface{}/any. You can assert from any interface type to a more specific type.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type ReadCloser interface {
    Reader
    Close() error
}

func process(r Reader) {
    // Check if this Reader also implements Close
    if rc, ok := r.(ReadCloser); ok {
        defer rc.Close()
    }

    // Check if this Reader implements WriteTo (for efficient copying)
    if wt, ok := r.(io.WriterTo); ok {
        wt.WriteTo(os.Stdout)
        return
    }

    // Fall back to regular Read
    buf := make([]byte, 1024)
    r.Read(buf)
}

Type Switch

A type switch branches based on the dynamic type of an interface value.

func describe(i interface{}) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("integer: %d", v)
    case string:
        return fmt.Sprintf("string: %q (len=%d)", v, len(v))
    case bool:
        return fmt.Sprintf("boolean: %t", v)
    case []int:
        return fmt.Sprintf("int slice: %v (len=%d)", v, len(v))
    case nil:
        return "nil"
    default:
        return fmt.Sprintf("unknown type: %T", v)
    }
}

fmt.Println(describe(42))        // "integer: 42"
fmt.Println(describe("hello"))   // "string: \"hello\" (len=5)"
fmt.Println(describe(nil))       // "nil"

Multiple Types in a Case

switch v := i.(type) {
case int, int8, int16, int32, int64:
    // v is interface{} here, NOT the concrete type
    // because multiple types could match
    fmt.Printf("some integer: %v\n", v)
case string, []byte:
    fmt.Printf("string-like: %v\n", v)
}

Multi-Type Case Variable

When a case lists multiple types, the variable v remains the interface type, not a concrete type. You lose type-specific operations. Prefer separate cases when you need to use type-specific methods.


Asserting Interface Satisfaction

Type assertions can check if a value implements a specific interface.

type Stringer interface {
    String() string
}

type Logger interface {
    Log(msg string)
}

func printIfStringable(v interface{}) {
    if s, ok := v.(Stringer); ok {
        fmt.Println(s.String())
    } else {
        fmt.Printf("%v\n", v)
    }
}

// Type switch on interfaces
func categorize(v interface{}) {
    switch v.(type) {
    case Stringer:
        fmt.Println("implements Stringer")
    case Logger:
        fmt.Println("implements Logger")
    case error:
        fmt.Println("is an error")
    default:
        fmt.Println("no known interface")
    }
}

Extracting Specific Behavior (Common Pattern)

This pattern is used extensively in the standard library to optionally use enhanced capabilities.

type Flusher interface {
    Flush() error
}

type Sizer interface {
    Size() int64
}

func writeData(w io.Writer, data []byte) error {
    _, err := w.Write(data)
    if err != nil {
        return err
    }

    // Optionally flush if the writer supports it
    if f, ok := w.(Flusher); ok {
        return f.Flush()
    }
    return nil
}

// Real example: net/http checks if ResponseWriter implements http.Flusher
func streamHandler(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "streaming not supported", http.StatusInternalServerError)
        return
    }

    for i := 0; i < 10; i++ {
        fmt.Fprintf(w, "data: %d\n\n", i)
        flusher.Flush() // send chunk immediately
        time.Sleep(1 * time.Second)
    }
}

Interface Conversion vs Type Assertion

type Animal interface {
    Speak() string
}

type Dog struct{ Name string }
func (d Dog) Speak() string { return "Woof!" }
func (d Dog) Fetch() string { return "Got it!" }

var a Animal = Dog{Name: "Rex"}

// Type assertion -- extract concrete type Dog
dog := a.(Dog)
fmt.Println(dog.Fetch()) // "Got it!" -- access Dog-specific methods

// Interface conversion -- this is compile-time, not runtime
var s fmt.Stringer = dog // Dog must implement Stringer at compile time
Operation Syntax When Fails
Type assertion x.(ConcreteType) Runtime Panic or ok=false
Interface assertion x.(InterfaceName) Runtime Panic or ok=false
Interface conversion var i Interface = value Compile time Compile error

Error Type Checking with Type Assertions

type NotFoundError struct {
    ID int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("resource %d not found", e.ID)
}

func handleError(err error) {
    // Type assertion approach
    if nfe, ok := err.(*NotFoundError); ok {
        log.Printf("not found: ID=%d", nfe.ID)
        return
    }

    // Preferred in modern Go: errors.As (handles wrapping)
    var nfe *NotFoundError
    if errors.As(err, &nfe) {
        log.Printf("not found: ID=%d", nfe.ID)
        return
    }

    log.Printf("unexpected error: %v", err)
}

errors.As vs Type Assertion

For error handling, prefer errors.As over type assertions. errors.As unwraps error chains created with fmt.Errorf("...: %w", err), while a direct type assertion only checks the outermost error.


Quick Reference

Concept Syntax Notes
Type assertion v := x.(T) Panics if x doesn't hold T
Safe assertion v, ok := x.(T) ok=false if wrong type, no panic
Type switch switch v := x.(type) { case T: ... } Branch on dynamic type
Multi-type case case T1, T2: v remains interface type
Nil case case nil: Matches nil interface value
Default case default: No type matched
Interface check v, ok := x.(InterfaceName) Check interface satisfaction
%T verb fmt.Sprintf("%T", x) Print dynamic type name

Best Practices

  1. Always use comma-ok for type assertions on values of uncertain type — bare assertions panic
  2. Prefer type switches over chains of if/else type assertions — cleaner and easier to extend
  3. Use errors.As instead of type assertions for error types — handles wrapped errors
  4. Keep type switches short — if you have many cases, consider redesigning with interfaces
  5. Assert to interfaces, not concrete types when possible — check for behavior, not identity
  6. Use type assertions to extract optional behavior — the io.WriterFlusher pattern is idiomatic
  7. Avoid interface{} when a specific interface will do — type assertions are a sign of lost type information

Common Pitfalls

Bare Assertion Panic

var i interface{} = "hello"
n := i.(int) // PANIC: interface conversion: interface {} is string, not int
Always use n, ok := i.(int) unless you're absolutely certain of the type.

Asserting on Nil Interface

var i interface{} // nil interface
s := i.(string)   // PANIC: interface conversion: interface is nil, not string
s, ok := i.(string) // ok=false, s="" -- safe
Nil interface values fail all type assertions. Check for nil first or use comma-ok.

Pointer vs Value Type Confusion

type MyError struct{ Msg string }
func (e *MyError) Error() string { return e.Msg }

var err error = &MyError{"oops"}

// WRONG -- MyError doesn't implement error, *MyError does
_, ok := err.(MyError)   // ok=false

// RIGHT
_, ok := err.(*MyError)  // ok=true
If methods are defined on *T, assert to *T, not T.

Type Switch Doesn't Fall Through

switch v := x.(type) {
case int:
    fmt.Println("int")
    // does NOT fall through to the next case
case string:
    fmt.Println("string")
}
Unlike C-style switches, Go type switches never fall through. The fallthrough keyword is not allowed in type switches.

Shadowing in Type Switch

switch v := x.(type) {
case int:
    v++ // v is int here
case string:
    v += "!" // v is string here -- same variable name, different type!
}
// v is not accessible here (scoped to switch)
The variable v is re-bound with a different type in each case — this is intentional but can be confusing.


Performance Considerations

Scenario Performance
Type assertion (concrete type) Very fast — single pointer comparison of type metadata
Type assertion (interface) Slightly slower — must check method set
Comma-ok assertion Same cost as bare assertion (no exception overhead)
Type switch (few cases) Compiled as a sequence of type checks — fast
Type switch (many cases) Linear scan — consider redesigning with polymorphism
reflect.TypeOf Much slower — avoid in hot paths, prefer type assertions
fmt.Sprintf("%T", x) Uses reflection internally — slow, use for debugging only

No Exception Overhead

Unlike exception-based languages, the comma-ok pattern has zero overhead compared to the panicking form. The "ok" boolean is a normal return value, not a caught exception.


Interview Tips

Interview Tip

"What's the difference between a type assertion and a type switch?" A type assertion (x.(T)) tests for a single specific type and extracts the value. A type switch (switch x.(type)) branches on the dynamic type among multiple possibilities. Use assertions for a known expected type; use switches when multiple types are possible.

Interview Tip

"When does a type assertion panic?" A bare assertion x.(T) panics when the interface value doesn't hold type T. The comma-ok form v, ok := x.(T) never panics — it returns ok=false and the zero value of T instead. Always use comma-ok for safety.

Interview Tip

"How are type assertions related to interfaces?" Type assertions are the runtime complement to Go's compile-time interface system. They let you "recover" type information that was erased when a value was stored in an interface. You can assert to a concrete type (get the exact value back) or to another interface (check for additional capabilities).

Interview Tip

"What's the idiomatic way to check optional capabilities?" Assert to an interface that defines the optional capability. For example, net/http checks if a ResponseWriter also implements http.Flusher or http.Hijacker. This pattern lets types opt into additional behavior without changing the base interface.


Key Takeaways

  • Type assertions extract concrete types from interface values: v := x.(T)
  • Always use comma-ok (v, ok := x.(T)) unless the type is guaranteed — bare assertions panic
  • Type switches (switch x.(type)) are cleaner than chains of type assertions
  • Multi-type cases (case int, int64:) keep the variable as the interface type
  • Assert to interfaces to check for behavior, not concrete types — more flexible
  • Type assertions are fast — they compare type metadata pointers, not values
  • For errors, prefer errors.As over type assertions — it handles wrapped error chains
  • fallthrough is not allowed in type switches
  • If you need many type assertion cases, reconsider your design — interfaces should provide polymorphism