Fix

"interface conversion: interface is nil, not X"

Fix the 'interface conversion: interface is nil' panic by checking if the interface is nil before performing a type assertion.

The panic that looks impossible

You are building a configuration loader. You pass a generic value through a handler, verify it is not nil, and then cast it to your expected struct. The program crashes with a runtime panic that says the interface is nil, not your struct. You already checked for nil. The code looks correct. The panic feels like a compiler bug. It is not. The crash comes from a fundamental detail about how Go represents interface values in memory.

How Go actually stores interfaces

Go interfaces are not empty boxes. Every interface value carries two pieces of information: a concrete type and a concrete value. Think of it like a shipping manifest. The manifest records what kind of item is inside and points to the actual item. If you have no manifest at all, you have a nil interface. If you have a manifest that explicitly says the item is a nil pointer, you have an interface that holds a nil value. These two states look identical when you print them, but the runtime treats them differently. A type assertion checks the manifest first. If the manifest does not exist, the assertion fails before it ever looks at the contents.

The minimal reproduction

package main

import "fmt"

// DemonstrateNilInterface shows the panic and the safe alternative.
// It highlights the difference between an empty interface and an interface holding nil.
func DemonstrateNilInterface() {
    var raw interface{} // raw is a nil interface: (type=nil, value=nil)
    
    // This panics at runtime because raw has no type information.
    // The assertion expects a string, but finds an empty manifest.
    // s := raw.(string) 

    // Check the interface itself before asserting.
    // This prevents the panic by verifying the manifest exists.
    if raw == nil {
        fmt.Println("interface is empty")
        return
    }
    
    // Use the two-value form to handle type mismatches gracefully.
    // The ok flag tells you whether the assertion succeeded.
    if s, ok := raw.(string); ok {
        fmt.Println("got string:", s)
    }
}

Walking through the runtime check

Type assertions are dynamic operations. The compiler generates a runtime check that compares the stored type against the requested type. If the stored type is nil, the comparison fails immediately. The runtime throws the panic because it cannot convert a missing type into a concrete one. This is why raw == nil works: it checks both the type and value slots simultaneously. When both are nil, the expression evaluates to true. When the type slot holds a pointer type but the value slot holds nil, the expression evaluates to false. The interface exists. The pointer inside it just points nowhere.

The panic message reads interface conversion: interface is nil, not string. It appears when you force a type assertion on an empty interface. The runtime cannot guess what type you intended. You must verify the interface holds a value first. A common trap involves pointers. If you assign a nil pointer to an interface, the interface itself is not nil. The type slot records the pointer type. The value slot records nil. A direct x == nil check returns false. The type assertion succeeds, but you get a nil pointer. Dereferencing it causes a different panic: invalid memory address or nil pointer dereference. Always separate interface nil checks from pointer nil checks.

Why the compiler stays silent

Go is statically typed, but interfaces are designed to be dynamic. The compiler knows that interface{} can hold anything. It cannot prove at compile time whether a variable will be assigned a concrete value or left uninitialized. The compiler also cannot track whether a function returns a nil interface or an interface wrapping a nil pointer. Both are valid interface{} values. The compiler generates the type assertion bytecode and lets the runtime decide. This design keeps the language simple and avoids complex flow analysis. It also means you must handle the empty manifest case explicitly.

Convention aside: The Go community prefers the two-value assertion for control flow. It mirrors the if err != nil pattern that dominates error handling. Both patterns make the failure path explicit and keep the happy path readable. You will see v, ok := x.(T) in standard library code and production systems. It is the idiomatic way to handle dynamic types. The compiler will not force you to use it, but production code relies on it heavily.

A realistic middleware example

package main

import (
    "fmt"
    "net/http"
)

// ExtractUser extracts a user identifier from the request context.
// It handles missing keys, nil values, and type mismatches safely.
func ExtractUser(r *http.Request) string {
    // Context values are always stored as interface{} (or any).
    // Accessing a missing key returns the zero value for the map type.
    raw := r.Context().Value("user")
    
    // Check if the context key was never set at all.
    // This catches the empty manifest case before asserting.
    if raw == nil {
        return "anonymous"
    }
    
    // Assert to the expected string type.
    // The two-value form prevents a panic on type mismatch.
    if id, ok := raw.(string); ok {
        return id
    }
    
    // Handle cases where the value exists but has the wrong type.
    // This might happen if a previous middleware stored an int or struct.
    fmt.Printf("unexpected user type: %T\n", raw)
    return "anonymous"
}

// MiddlewareChain demonstrates how ExtractUser fits into a handler.
// It shows the pattern in a realistic request flow.
func MiddlewareChain(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // Extract the user safely before calling the next handler.
        // This avoids panics if the context is incomplete.
        userID := ExtractUser(r)
        
        // Log the extracted identifier for debugging purposes.
        // This helps track requests without crashing the server.
        fmt.Println("request user:", userID)
        
        // Pass control to the next handler in the chain.
        // The request object remains unchanged for downstream handlers.
        next(w, r)
    }
}

Pitfalls and runtime behavior

The panic interface conversion: interface is nil, not X is a runtime error, not a compile error. The compiler sees a valid type assertion syntax and trusts you. The runtime catches the mismatch and stops execution. This behavior protects your program from silently using garbage data. It also means you cannot rely on the compiler to catch missing nil checks. You must write them yourself.

Another trap involves type aliases. any is just an alias for interface{}. The runtime treats them identically. If you see any in modern code, remember it still carries the two-slot structure. The panic message will still reference interface because that is the underlying type. Do not assume any behaves differently. It follows the exact same rules.

Convention aside: Public names start with a capital letter. Private start lowercase. When you define custom interfaces, keep them small and focused. Large interfaces that require constant type assertions usually indicate a design that should use concrete structs instead. Accept interfaces at function boundaries. Return concrete values. This pattern reduces the number of runtime assertions you need to write.

When to use which approach

Use a direct type assertion x.(T) when you are certain the value matches and you want a fast panic on developer error. Use the two-value assertion v, ok := x.(T) when the input comes from external sources like JSON, databases, or user configuration. Use an x == nil check before any assertion when the interface might be completely unassigned. Use a type switch switch v := x.(type) when you need to handle multiple possible concrete types in the same block. Use a concrete struct or pointer instead of an interface when you know the type at compile time.

Where to go next