Interface Values

The internal two-word structure of Go interface values, the dynamic type and value pair, how nil interfaces differ from interfaces holding a nil concrete value, and how to avoid the most common nil-check errors.

The Two-Word Internal Layout

Every interface value in Go stores two pieces of information under the hood: the concrete type of the data it holds and a pointer to that data. You can think of an interface value as a pair:

(concrete type, concrete value)

The first element describes what kind of data is inside. The second element is the data itself — or, when the data is larger than a machine word, a pointer to a heap-allocated copy of it. This pair is the reason an interface can behave as if it “contains” any type that satisfies its method set.

These two parts are often called the dynamic type and the dynamic value. They are “dynamic” because they are not fixed at compile time; they change whenever you assign a new concrete value to the interface variable.

A helpful mental model:

Picture an interface variable as an envelope with two slots. One slot holds a label telling you the exact concrete type (like *os.File or int). The other slot holds the actual data. When the envelope is empty, both slots are blank — that is a nil interface. When you put something inside, you fill both slots.

How Go Stores the Concrete Data

When you assign a concrete value to an interface variable, Go does more than just wrap it. Let’s start with a simple example.

type Stringer interface {
    String() string
}
type Person struct {
    First, Last string
}
func (p Person) String() string {
    return p.First + " " + p.Last
}
func main() {
    var s Stringer
    p := Person{"Ada", "Lovelace"}
    s = p
    fmt.Println(s.String())
}

The assignment s = p causes the runtime to store the following inside s:

  • Dynamic type: main.Person
  • Dynamic value: a copy of the Person struct {Ada, Lovelace}

The original variable p and the interface s now hold independent copies. If you change p.First later, s is unaffected. That’s the same behaviour you get with ordinary assignment in Go — interface assignment is value-oriented.

Larger types, or any type whose size the compiler cannot guarantee fits inside the interface’s data slot, are copied to the heap. The second slot of the interface stores a pointer to that copy. You never see this pointer directly; the Go runtime manages it.

Calling s.String() works by looking up the method table associated with the concrete type (through the first slot) and then passing the data from the second slot to the method. The whole process is safe and fast because the itable — the internal table that maps interface methods to concrete method implementations — is computed once when the interface value is first created and then cached.

Everything is a copy:

If you assign a concrete value to an interface, you get an independent copy. Modifying the original variable afterwards does not alter the interface’s value. This is exactly what you’d expect from Go’s value semantics — it just happens invisibly inside the interface pair.

A Nil Interface Value

A variable of an interface type that has never been assigned a concrete value contains no type and no value. Both slots are empty. In Go, that state is nil.

var s Stringer
fmt.Println(s == nil) // true

A nil interface is genuinely empty. You cannot call any method on it; attempting to do so causes a runtime panic because there is no concrete receiver.

var s Stringer
s.String() // panic: runtime error: invalid memory address or nil pointer dereference

This panic is straightforward: there is no data and no method table, so the call has nowhere to go.

Calling a method on a nil interface panics immediately:

Unlike calling a method on a nil pointer receiver (which can work if the method doesn’t dereference the receiver), a nil interface cannot be used at all. Always check if s != nil before making a call if the interface may be uninitialized.

When an Interface Holds a Nil Concrete Value

Now here is the distinction that trips up almost every Go developer at some point: an interface can hold a nil concrete pointer and yet not be a nil interface.

The dynamic type slot still contains a concrete type (like *Person), while the dynamic value slot holds nil. Because the type slot is non-nil, the interface as a whole is non-nil.

var p *Person       // p is a nil pointer of type *Person
var s Stringer = p  // s holds (type=*Person, value=nil)
fmt.Println(p == nil) // true
fmt.Println(s == nil) // false — the interface is not nil!

s == nil returns false because the runtime sees a non-nil type component. An interface is only nil when both components are nil.

Calling s.String() now does not panic on the interface call itself. The runtime finds the method table for *Person and invokes the method. But inside the method, the receiver p is nil. If the method dereferences p (for example, accessing p.First), the program panics with a nil pointer dereference.

s.String() // panic: runtime error: invalid memory address or nil pointer dereference

The panic happens inside the method body, not at the call site. This can make debugging more confusing, especially when the interface value comes from a function return.

The Classic error Interface Trap

The most famous real-world version of this problem involves the error interface. A function returns a concrete nil pointer typed as a custom error, and the caller checks for nil on the returned error interface.

type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func mightFail() error {
    var err *MyError // nil
    // some condition that never triggers an error
    return err
}
func main() {
    if err := mightFail(); err != nil {
        fmt.Println("error occurred:", err) // This prints!
    }
}

The function mightFail returns an error interface value whose dynamic type is *MyError and whose dynamic value is nil. The interface is not nil, so the caller erroneously believes an error occurred. The fix is to return the untyped nil directly when there is no error:

func mightFail() error {
    return nil // correct: both type and value are nil
}

Never return a typed nil pointer as an error:

Returning a nil pointer of a concrete error type creates a non-nil error interface. Always return nil for the no-error case. This is the single most common interface-value bug in Go production code.

Distinguishing the Two Kinds of nil

To check whether an interface value is truly empty, use s == nil. To check whether the concrete value inside is nil (while the interface itself is non-nil), you must use a type assertion or reflection. But the rule of thumb is simple:

  • If you never assigned anything, the interface is nil.
  • If you assigned a typed nil pointer, the interface is not nil, and any code that treats it as nil will be wrong.

Understanding this two-word model is what makes those later tools (type assertions, type switches) intuitive. They all operate by inspecting the dynamic type and value that are already sitting inside the interface.

Summary

Interface values in Go are not magic; they are a pair of a concrete type pointer and a data pointer (or a copy of a small value). This simple representation is what makes interfaces flexible enough to hold any type that satisfies their method set, while still supporting compile-time safety.

The real dividing line is the difference between a nil interface (both slots empty) and an interface that wraps a nil concrete pointer (type slot filled, value slot nil). That distinction directly affects nil checks on error returns and is the root of a large fraction of interface-related bugs.

Once you internalize the two-word picture, type assertions and type switches become a natural way to peek inside the envelope and recover the concrete data.