arthrop0d

Brief articles on varied topics.

Go Interface Values: Dynamic Types, Values, and Nil Pointers

golangcomputer science

An interface value in Go is a pair: a dynamic type and a dynamic value. The dynamic type says which concrete type is currently stored; the dynamic value is the data for that type. An interface is nil only when both parts are absent.

This model lets code call methods through an interface without knowing the concrete type behind it. It also explains a common surprise: an interface containing a nil pointer is not itself nil.

The problem interfaces solve

Suppose a function needs to write output. Without interfaces, it might need separate versions for a file, a network connection, and an in-memory buffer. Each version would know the concrete type it receives, and callers would be tied to those implementations.

An interface describes behavior instead: a type satisfies an interface when it has the required methods. Code can then depend on that behavior rather than on one particular implementation.

For example, io.Writer represents something with a Write method. A file, buffer, or network connection can all be passed to code that accepts an io.Writer, even though their data structures and implementations differ.

The interface is therefore a stable boundary between a caller and multiple implementations. The caller asks for an operation; the concrete value supplies the implementation.

What an interface stores

Consider this code:

type Greeter interface {
    Greet() string
}

type English struct{}

func (English) Greet() string {
    return "hello"
}

var g Greeter = English{}

After the assignment, g has:

The variable's declared type is Greeter, but the value currently stored in it is an English value. The declared interface type determines which methods callers may use; the dynamic type identifies which implementation will run.

The Go language defines the behavior in terms of this type-and-value pair. In the usual runtime implementation, an interface is represented using two machine words, though this is an implementation detail rather than a representation that programs should depend on.

For an interface with methods, one word points to information describing the dynamic type and its relationship to the interface. This information includes the function entries needed for the interface's methods. The other word points to, or contains, the dynamic value. For an empty interface—such as any, which is an alias for interface{}—there are no methods to dispatch, so the runtime needs type information and the value but not an interface-method table.

The exact layout can vary with compiler and runtime optimizations. The useful mental model is still type plus value.

How method dispatch uses the pair

When code calls a method through an interface, it does not select a function solely from the interface variable's declared type. The interface's dynamic type determines the implementation.

type Greeter interface {
    Greet() string
}

type English struct{}
type Spanish struct{}

func (English) Greet() string { return "hello" }
func (Spanish) Greet() string { return "hola" }

func welcome(g Greeter) string {
    return g.Greet()
}

Calling welcome(English{}) and welcome(Spanish{}) uses the same welcome function, but the two calls produce different results. Conceptually, the call proceeds like this:

  1. Read the dynamic-type information from the interface value.
  2. Find the method entry corresponding to Greet.
  3. Call that function with the interface's dynamic value as its receiver.

That lookup-and-call operation is dynamic dispatch: the implementation is chosen at run time from the value being passed. The compiler can sometimes optimize a particular call when it can prove the concrete type, but the language behavior remains the same.

The dynamic value is important because the selected method needs a receiver. If the concrete value is a struct, the receiver data comes from the value stored in the interface. If the concrete value is a pointer, the receiver is that pointer.

Why a typed nil is not a nil interface

A nil interface has no dynamic type and no dynamic value:

var g Greeter
fmt.Println(g == nil) // true

By contrast, this assigns a nil pointer to an interface:

type NotFound struct{}

func (*NotFound) Error() string {
    return "not found"
}

var p *NotFound = nil
var err error = p

fmt.Println(err == nil) // false

Here, err contains:

The interface is not nil because its type part is present. The pointer stored as its value happens to be nil, but that is different from the interface having no value at all.

This is the source of a familiar error-handling bug:

func lookup() error {
    var e *NotFound = nil
    return e
}

if err := lookup(); err != nil {
    // This branch runs: err contains a typed nil pointer.
}

A method call through such an interface can still be dispatched. The method receives a nil *NotFound receiver. If the method checks for nil, it may work; if it dereferences the receiver, it may panic. The interface's non-nil status does not make the underlying pointer non-nil.

The practical rule is to return a plain nil interface when a function means “no error,” rather than converting a nil pointer to error. When diagnosing logs or conditions involving err != nil, remember that the comparison examines the interface pair, not just whether the underlying pointer is nil.

← All articles