The Nil Interface Gotcha in Go (Non-Nil Interface Containing Nil Value)

An interface is non-nil if it holds a type, even if that type's value is nil; check the underlying value with reflection or explicit type assertions.

The nil interface gotcha

You write a function that returns an interface. You check if result != nil. The check passes. You call a method on result. The program crashes with a nil pointer dereference. The check said the value was there, but the runtime says it isn't. This contradiction happens because a Go interface holds two pieces of information: a concrete type and a value. The interface can be non-nil even if the value inside is a nil pointer.

How interfaces store data

An interface variable is a box. Inside the box, there is a label and a payload. The label tells you the concrete type, like *MyStruct or string. The payload holds the actual data. If the box is completely empty, the interface is nil. If the box has a label but the payload is a nil pointer, the box is not empty. The interface is non-nil. The label exists. The payload is just a null reference.

Go represents an interface value internally as a pair of words. One word points to the type descriptor. The other word holds the data. If both words are zero, the interface is nil. If the type word is non-zero, the interface is non-nil. The comparison i != nil checks both words. It finds a type, so it returns true. The value inside might be nil, but the interface itself is not.

Minimal example

Here's the smallest case that triggers the gotcha.

package main

import "fmt"

type MyType struct {
	Name string
}

func main() {
	// Assign a nil pointer to an interface.
	// The interface now knows the type is *MyType, but the value is nil.
	var i interface{} = (*MyType)(nil)

	// The interface is not nil because it holds a type.
	if i != nil {
		fmt.Println("Interface is non-nil")
	}

	// Accessing the value directly would panic if we tried to dereference.
	// The type information is present, even if the pointer is nil.
	fmt.Printf("Type: %T, Value: %v\n", i, i)
}
# output:
Interface is non-nil
Type: *main.MyType, Value: <nil>

The output shows the interface is non-nil. The type is *main.MyType. The value prints as <nil>. The box has a label. The payload is empty.

Realistic scenario

This pattern often appears in factory functions or constructors. A function returns an interface, but returns a nil pointer of a concrete type when something goes wrong.

package main

import "fmt"

type Shape interface {
	Area() float64
}

type Circle struct {
	Radius float64
}

func (c *Circle) Area() float64 {
	return 3.14 * c.Radius * c.Radius
}

// GetShape returns a Shape.
// It returns a nil pointer when radius is invalid.
func GetShape(radius float64) Shape {
	if radius < 0 {
		// Return a nil *Circle.
		// The return type is Shape, so this becomes a non-nil interface.
		return (*Circle)(nil)
	}
	return &Circle{Radius: radius}
}

func main() {
	shape := GetShape(-1)

	// This check passes because the interface holds the type *Circle.
	if shape != nil {
		// This panics because shape is a nil pointer.
		fmt.Println(shape.Area())
	}
}
# output:
panic: runtime error: invalid memory address or nil pointer dereference

The runtime panics with invalid memory address or nil pointer dereference when you call a method on a nil pointer wrapped in an interface. The method receiver is nil. The runtime tries to access memory at address zero.

The fix is to return nil directly instead of a typed nil pointer.

func GetShape(radius float64) Shape {
	if radius < 0 {
		// Return a nil interface.
		// The box is empty. The check if shape != nil will fail.
		return nil
	}
	return &Circle{Radius: radius}
}

Return nil, not (*T)(nil). Let the interface be empty.

Checking the underlying value

Sometimes you need to distinguish between a nil interface and a non-nil interface containing a nil pointer. This happens when you are writing generic code or debugging. You have two options: type assertion or reflection.

Type assertion is the idiomatic approach. It extracts the concrete value and lets you compare it to nil.

package main

import "fmt"

func main() {
	var i interface{} = (*MyType)(nil)

	// Check if the underlying value is a nil *MyType.
	// The type assertion succeeds, and v is the nil pointer.
	if v, ok := i.(*MyType); ok && v == nil {
		fmt.Println("Interface holds a nil *MyType")
	}
}

type MyType struct{}

Reflection is the sledgehammer. It works for any type but adds overhead.

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var i interface{} = (*MyType)(nil)

	// reflect.ValueOf creates a reflect.Value.
	// IsNil checks the underlying value, not the interface.
	if reflect.ValueOf(i).IsNil() {
		fmt.Println("Underlying value is nil")
	}
}

type MyType struct{}

reflect.ValueOf(i).IsNil() returns true for pointers, interfaces, maps, slices, channels, and functions. It panics if the value is not one of those kinds. Use reflection sparingly. It allocates and is slower than a direct check.

Why Go works this way

Go keeps the type and value separate to support dynamic dispatch. The type is needed to find the method table. If the interface were nil when the value was nil, you couldn't distinguish between "no value" and "value of type T that happens to be nil". Some code relies on this distinction. A function might accept an interface and check if the underlying value is a nil pointer to decide whether to allocate a new one.

The error interface is the most common victim of this pattern. The community convention is to return nil on success, which creates a nil interface. If a function returns (*MyError)(nil), the interface is non-nil. The check if err != nil triggers. Printing the error shows <nil>. This confuses callers and breaks logging. Always return nil directly, not a typed nil pointer. The boilerplate if err != nil is verbose by design. It makes the unhappy path visible. Don't break it with hidden nil pointers.

Pitfalls and compiler behavior

The compiler does not catch this bug. It sees a valid assignment. You are assigning a *MyType to an interface{}. The types match. The compiler allows it. The bug appears at runtime.

The compiler rejects programs with loop variable i captured by func literal if you forget to capture loop variables in goroutines. It complains with cannot use x (untyped int constant) as string value in argument if you pass the wrong type. It does not warn about nil pointers inside interfaces. You have to check for this yourself.

The worst goroutine bug is the one that never logs. The worst interface bug is the one that passes the nil check. Always test the nil path. Return nil when you mean absence. Use type assertions when you need to inspect the payload.

Decision matrix

Use a direct nil return when the function indicates absence by returning a nil interface. Use a type assertion to check the underlying value when you need to distinguish between a nil interface and a non-nil interface containing a nil pointer. Use reflect.ValueOf(i).IsNil() when you are writing generic utility code and cannot know the concrete type at compile time. Use an error return alongside the value when the caller needs to know why a value is missing, rather than guessing from a nil pointer. Use a struct with a boolean flag when you need to represent "present but zero" versus "absent" without pointer indirection.

Clarity beats cleverness. If the caller needs to check for nil, make the nil check obvious.

Where to go next