Go Interface Values: Dynamic Types, Values, and Typed Nil
Go interface values store two things: a dynamic type and a dynamic value. An interface is equal to nil only when both are absent. That is why an interface containing a typed nil—such as a nil pointer—can be non-nil.
This distinction is small, but it explains familiar Go bugs: an error that appears to contain no error still compares unequal to nil, or a method call unexpectedly panics after a nil check.
The problem interfaces solve
An interface type describes behavior through a set of methods, rather than identifying one concrete representation. Any type that provides those methods can be used wherever the interface is expected.
For example, code can accept a Notifier without knowing whether the implementation is a client, a logger, or a test double:
type Notifier interface {
\tNotify()
}
func send(n Notifier) {
\tn.Notify()
}
Without interfaces, send would need to accept one particular concrete type, or separate functions for every type that can notify. Interfaces let the caller choose the implementation while the function depends only on the behavior it needs.
That flexibility requires a runtime representation for “some value that satisfies this interface.” Go represents that conceptually as a pair:
interface value = dynamic type + dynamic value
The dynamic type is the concrete type currently stored in the interface. The dynamic value is the value of that concrete type.
Assigning a concrete value to an interface
type Client struct {
\tName string
}
func (c Client) Notify() {}
var c Client = Client{Name: "payments"}
var n Notifier = c
After the assignment to n:
n's dynamic type isClient.n's dynamic value isClient{Name: "payments"}.
The variable n has the static type Notifier: that is the type the compiler sees at the declaration. Its dynamic type is the concrete type stored inside it at runtime.
This is also what makes type assertions possible. A type assertion asks an interface for its stored value as a particular concrete type:
client, ok := n.(Client)
if ok {
\tfmt.Println(client.Name)
}
The assertion succeeds because the interface's dynamic type is Client.
What nil means for an interface
The zero value of an interface is nil:
var n Notifier
fmt.Println(n == nil) // true
This interface has neither a dynamic type nor a dynamic value. It is an empty pair.
Assigning a typed nil changes the situation:
var c *Client = nil
var n Notifier = c
fmt.Println(n == nil) // false
Here, c is a nil pointer. But after assigning it to n, the interface contains:
- dynamic type:
*Client - dynamic value:
nil
The interface itself is therefore not nil. It contains type information even though the value associated with that type is nil.
A useful mental model is that these are different states:
| Interface state | Dynamic type | Dynamic value | interface == nil |
|---|---|---|---|
| Nil interface | absent | absent | true |
| Interface holding a pointer | *Client |
nil | false |
| Interface holding a pointer | *Client |
address of a Client |
false |
The comparison with nil is checking whether the interface has no stored type and value, not whether the stored value alone is nil.
The common error example
The same behavior often appears in error handling. error is an interface type with an Error() string method:
type MyError struct{}
func (*MyError) Error() string {
\treturn "operation failed"
}
func load() error {
\tvar err *MyError = nil
\treturn err
}
func main() {
\terr := load()
\tif err != nil {
\t\tfmt.Println("an error occurred")
\t}
}
The if branch runs. The returned error has dynamic type *MyError and dynamic value nil, so the interface is non-nil.
The usual fix is to return a nil interface when there is no error:
func load() error {
\t// ...
\treturn nil
}
If an operation may fail with a pointer-based error, construct and return that error only on the failure path. Do not place a typed nil pointer into the error interface when reporting success.
What happens when you use the value
A non-nil interface containing a nil pointer can still be passed around and can sometimes call methods. Whether the call works depends on the method. A method that does not dereference its pointer receiver may run; a method that reads through the nil pointer may panic.
That means this check is not enough to prove that the underlying pointer is usable:
if n != nil {
\tn.Notify()
}
It proves only that the interface has a dynamic value of some type. If that type is a nil pointer, the method must handle a nil receiver or the program must avoid creating that interface value.
The key rule is simple: a nil interface has no dynamic type; an interface containing a typed nil does have one. Once you picture an interface as a type-and-value pair, the surprising comparison and the related error bugs become predictable.