Problem Statement
A production bug at Cloudflare caused a nil pointer panic despite a nil check passing. The root cause was Go's interface implementation. Explain the internal structure of interfaces and the infamous "nil interface" trap.
Interface Internal Structure
// Empty interface (interface{} or any)
type eface struct {
_type *_type // Type information
data unsafe.Pointer // Pointer to actual value
}
// Non-empty interface (has methods)
type iface struct {
tab *itab // Type + method table
data unsafe.Pointer // Pointer to actual value
}The Nil Interface Trap
package main
import "fmt"
type MyError struct {
msg string
}
func (e *MyError) Error() string {
return e.msg
}
func mayFail(fail bool) error {
var err *MyError = nil
if fail {
err = &MyError{"something went wrong"}
}
return err // ⚠️ Returns non-nil interface with nil data!
}
func main() {
err := mayFail(false)
// This check PASSES even though err.data is nil
if err != nil {
fmt.Println("Error:", err.Error()) // PANIC: nil pointer
}
}Why This Happens
When mayFail returns:
err (interface) = {
type: *MyError ← NOT nil!
data: nil ← The actual value is nil
}
An interface is only nil when BOTH type and data are nil.The Fix
func mayFail(fail bool) error {
if fail {
return &MyError{"something went wrong"}
}
return nil // Return untyped nil
}
// Or check explicitly:
func isNilInterface(i interface{}) bool {
if i == nil {
return true
}
switch v := reflect.ValueOf(i); v.Kind() {
case reflect.Ptr, reflect.Map, reflect.Slice,
reflect.Chan, reflect.Func, reflect.Interface:
return v.IsNil()
}
return false
}Interface Conversion Cost
// ❌ Expensive: []T to []interface{} requires copying
func toInterfaceSlice(s []int) []interface{} {
result := make([]interface{}, len(s))
for i, v := range s {
result[i] = v // Each element boxed individually
}
return result
}
// The memory layout is different:
// []int: [1][2][3][4][5] (contiguous ints)
// []interface{}: [iface][iface][iface] (array of fat pointers)Type Assertions vs Type Switches
func process(v interface{}) {
// Type assertion (single type)
if s, ok := v.(string); ok {
fmt.Println("String:", s)
}
// Type switch (multiple types)
switch x := v.(type) {
case int:
fmt.Println("Int:", x*2)
case string:
fmt.Println("String:", x)
case []byte:
fmt.Println("Bytes:", len(x))
default:
fmt.Printf("Unknown: %T\n", x)
}
}Follow-up Questions
- What's the overhead of an interface method call vs direct call?
- Can a value receiver satisfy an interface expecting a pointer receiver?
- How does Go determine interface satisfaction at compile time?