Fix

"interface conversion: X is not Y" in Go

Fix the 'interface conversion' panic in Go by using the comma-ok idiom to safely check types before asserting them.

The panic that breaks assumptions

You are building a service that processes messages from a queue. The handler accepts a generic interface{} so it can handle different payload shapes. You pass a map[string]interface{} and everything works. You refactor the producer to send a custom struct instead. Suddenly, the handler crashes with interface conversion: *main.Message is not map[string]interface{}. The program halts. The stack trace points to a line where you assumed the interface held a map.

This panic is the runtime telling you that your assumption about the data is wrong. Go interfaces are dynamic. They can hold any type. A type assertion is a claim that the interface holds a specific type. If the claim is false, the runtime panics. The panic message shows you exactly what went wrong: the type you have versus the type you demanded.

What an interface actually holds

Go interfaces are containers for two pieces of information: the concrete type and the concrete value. When you assign a value to an interface variable, Go boxes the value. The interface stores a reference to the type descriptor and a reference to the data. The type descriptor tells the runtime what kind of value is inside. The data reference points to the actual bytes.

A type assertion peeks at the type descriptor. When you write x.(TargetType), you are asking the runtime to check if the type descriptor matches TargetType. If it matches, the assertion returns the value. If it does not match, the assertion fails. The failure mode depends on how you write the assertion. A bare assertion panics. An assertion with the comma-ok idiom returns a boolean indicating success or failure.

Interfaces hold types. Assertions check types.

Safe extraction with the comma-ok idiom

The comma-ok idiom is the standard way to perform type assertions safely. It returns two values: the extracted value and a boolean. The boolean is true if the assertion succeeded and false if it failed. This pattern lets you handle the mismatch gracefully without crashing the program.

package main

import "fmt"

// Shape defines a geometric object.
type Shape interface {
    Area() float64
}

// Circle implements Shape.
type Circle struct {
    Radius float64
}

// Area returns the area of the circle.
func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

// Square implements Shape.
type Square struct {
    Side float64
}

// Area returns the area of the square.
func (s Square) Area() float64 {
    return s.Side * s.Side
}

func main() {
    var s Shape = Circle{Radius: 5}

    // This assertion claims s is a Circle. It is true.
    // The runtime checks the dynamic type and returns the value.
    if c, ok := s.(Circle); ok {
        fmt.Printf("Circle radius: %.2f\n", c.Radius)
    }

    // This assertion claims s is a Square. It is false.
    // The comma-ok idiom catches the mismatch without panicking.
    if sq, ok := s.(Square); ok {
        fmt.Println("Square side:", sq.Side)
    } else {
        fmt.Println("Not a square")
    }

    // This assertion claims s is a Square without the safety check.
    // The runtime detects the mismatch and panics immediately.
    // Uncommenting this line will crash the program.
    // _ = s.(Square)
}

The comma-ok idiom prevents crashes. Use it every time the type is in doubt.

How the runtime checks types

Under the hood, the runtime performs a comparison of type descriptors. When you create an interface value, the runtime allocates a structure that holds a pointer to a type object and a pointer to the data. The type object contains metadata about the concrete type, including its name, size, and method set.

During a type assertion, the runtime reads the type pointer from the interface and compares it to the type object of the target type. If the pointers match, the assertion succeeds. The runtime then returns the data pointer, possibly converting it to the requested type if needed. If the pointers do not match, the assertion fails.

This comparison is fast. It does not require reflection or string matching. The type information is baked into the binary. The cost of a type assertion is negligible compared to the cost of a panic. A panic unwinds the stack, prints a trace, and terminates the goroutine. Preventing panics with the comma-ok idiom is always cheaper than recovering from them.

Handling multiple types in production

In real code, you often need to handle several possible types. A series of type assertions works, but it can become verbose. Go provides a type switch for this scenario. A type switch branches based on the concrete type of the interface. It is cleaner and more efficient than multiple assertions.

package main

import (
    "fmt"
    "log"
)

// Payload represents a generic message body.
type Payload interface{}

// ProcessPayload handles different message types.
// It checks the concrete type to decide how to parse the data.
func ProcessPayload(p Payload) error {
    // Check if the payload is a JSON map.
    // This handles unstructured data from external APIs.
    if data, ok := p.(map[string]interface{}); ok {
        fmt.Println("Processing JSON map:", data)
        return nil
    }

    // Check if the payload is a string.
    // This handles simple text messages.
    if text, ok := p.(string); ok {
        fmt.Println("Processing text:", text)
        return nil
    }

    // If no type matches, return an error.
    // This prevents silent failures or panics downstream.
    return fmt.Errorf("unsupported payload type: %T", p)
}

// HandleWithSwitch demonstrates a type switch.
// It branches based on the concrete type of the interface.
func HandleWithSwitch(v interface{}) {
    switch t := v.(type) {
    case int:
        fmt.Println("Integer:", t)
    case string:
        fmt.Println("String:", t)
    case bool:
        fmt.Println("Boolean:", t)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    // Simulate receiving a map payload.
    err := ProcessPayload(map[string]interface{}{"key": "value"})
    if err != nil {
        log.Fatal(err)
    }

    // Simulate receiving a string payload.
    err = ProcessPayload("hello world")
    if err != nil {
        log.Fatal(err)
    }

    // Simulate an unexpected type.
    err = ProcessPayload(42)
    if err != nil {
        log.Println("Expected error:", err)
    }

    // Demonstrate type switch usage.
    HandleWithSwitch(123)
    HandleWithSwitch("test")
    HandleWithSwitch(true)
    HandleWithSwitch(struct{}{})
}

Type switches handle branching. Assertions extract values.

Common traps and compiler behavior

The most frequent cause of interface conversion panics is a mismatch between pointers and values. An interface holding a *Circle is not a Circle. The type includes the pointer star. If you assert s.(Circle) when the interface holds *Circle, the runtime panics with interface conversion: interface {} is *main.Circle, not main.Circle. You must match the exact type stored in the interface. If the interface holds a pointer, assert for a pointer. If it holds a value, assert for a value.

Another trap involves nil interfaces. An interface value is nil only when both its type and value are nil. If you assign a nil pointer to an interface, the interface is not nil. It holds a type (the pointer type) and a nil value. Asserting on a nil interface always fails. The comma-ok idiom handles this gracefully. The bare assertion panics. Always use the comma-ok idiom when the type is uncertain.

The panic message format is precise. The runtime prints interface conversion: X is not Y. X is the dynamic type stored in the interface. Y is the type you requested. If X is nil, the message says interface conversion: interface {} is nil, not Y. This tells you the interface was empty. If X is a pointer, the message includes the star. This precision helps you fix the bug quickly. You do not have to guess. The runtime tells you the mismatch.

The community convention is to treat type assertions as queries, not commands. Use _ to discard the value if you only need to check the type. if _, ok := x.(TargetType); ok signals you care about the type, not the value. This pattern is common when you need to verify a type before calling a method that is not part of the interface.

The mantra "accept interfaces, return structs" helps reduce type assertions. If a function returns a concrete struct, the caller knows the type and can access fields directly. If a function returns an interface, the caller is forced to assert if they need behavior beyond the interface definition. Designing functions to return structs pushes type assertions to the boundary of the system, where they are easier to manage.

Nil interfaces are not nil pointers. Check the type, not just the value.

Choosing the right tool

Use a type assertion with the comma-ok idiom when you need to extract a value from an interface and handle the case where the type might not match.

Use a type switch when you have multiple possible types and need to branch logic based on the concrete type.

Use a bare type assertion only when you are certain of the type and want the program to crash if your assumption is wrong, such as in internal code where a panic indicates a programming error.

Use method calls on the interface directly when you only need the behavior defined by the interface and do not need the underlying concrete type.

Use a custom type with a conversion function when you control both sides of the interface and can avoid assertions entirely by designing the interface to match your needs.

Design interfaces to avoid assertions. Return structs when you can.

Where to go next