Conceptually, a value of an interface type, or interface value, has two components, a concrete type and a value of that type. These are called the interface’s dynamic type and dynamic value.
For a statically typed language like Go, types are a compile-time concept, so a type is not a value. In our conceptual model, a set of values called type descriptors provide information about each type, such as its name and methods. In an interface value, the type component is represented by the appropriate type descriptor.
In the four statements below, the variable w
takes on
three different values. (The initial and final values are the same.)
var w io.Writer w = os.Stdout w = new(bytes.Buffer) w = nil
Let’s take a closer look at the value and dynamic behavior of w
after each statement.
The first statement declares w
:
var w io.Writer
In Go, variables are always initialized to a well-defined value,
and interfaces are no exception.
The zero value for an interface has both its type and value
components set to nil
(Figure 7.1).
An interface value is described as nil or non-nil based
on its dynamic type, so this is a nil interface value.
You can test whether an interface value is nil
using w == nil
or w != nil
.
Calling any method of a nil interface value causes a panic:
w.Write([]byte("hello")) // panic: nil pointer dereference
The second statement assigns a value of type *os.File
to w
:
w = os.Stdout
This assignment involves an implicit conversion from a concrete type
to an interface type, and is equivalent to the explicit conversion
io.Writer(os.Stdout)
.
A conversion of this kind, whether explicit or implicit, captures the
type and the value of its operand.
The interface value’s dynamic type is set to the
type descriptor for the pointer type *os.File
, and its dynamic
value holds a copy of os.Stdout
, which is a pointer to the
os.File
variable representing the standard output of the
process (Figure 7.2).
Calling the Write
method on an interface value containing an
*os.File
pointer causes the (*os.File).Write
method to
be called.
The call prints "hello"
.
w.Write([]byte("hello")) // "hello"
In general, we cannot know at compile time what the dynamic type
of an interface value will be, so a call through an interface must use
dynamic dispatch.
Instead of a direct call, the compiler must generate code to obtain
the address of the method named Write
from the type
descriptor, then make an indirect call to that address.
The receiver argument for the call is a copy of the interface’s
dynamic value, os.Stdout
.
The effect is as if we had made this call directly:
os.Stdout.Write([]byte("hello")) // "hello"
The third statement assigns a value of type *bytes.Buffer
to
the interface value:
w = new(bytes.Buffer)
The dynamic type is now *bytes.Buffer
and the dynamic value is
a pointer to the newly allocated buffer (Figure 7.3).
A call to the Write
method uses the same mechanism as before:
w.Write([]byte("hello")) // writes "hello" to the bytes.Buffer
This time, the type descriptor is *bytes.Buffer
, so the
(*bytes.Buffer).Write
method is called, with the address of the
buffer as the value of the receiver parameter.
The call appends "hello"
to the buffer.
Finally, the fourth statement assigns nil
to the interface value:
w = nil
This resets both its components to nil
, restoring w
to the
same state as when it was declared, which was shown in
Figure 7.1.
An interface value can hold arbitrarily large dynamic values.
For example, the time.Time
type, which represents an instant in
time, is a struct type with several unexported fields.
If we create an interface value from it,
var x interface{} = time.Now()
the result might look like Figure 7.4. Conceptually, the dynamic value always fits inside the interface value, no matter how large its type. (This is only a conceptual model; a realistic implementation is quite different.)
Interface values may be compared using ==
and !=
.
Two interface values are equal if both are nil, or if their dynamic
types are identical and their dynamic values are equal according to
the usual behavior of ==
for that type.
Because interface values are comparable, they may be used as
the keys of a map or as the operand of a switch statement.
However, if two interface values are compared and have the same dynamic type, but that type is not comparable (a slice, for instance), then the comparison fails with a panic:
var x interface{} = []int{1, 2, 3} fmt.Println(x == x) // panic: comparing uncomparable type []int
In this respect, interface types are unusual. Other types are either safely comparable (like basic types and pointers) or not comparable at all (like slices, maps, and functions), but when comparing interface values or aggregate types that contain interface values, we must be aware of the potential for a panic. A similar risk exists when using interfaces as map keys or switch operands. Only compare interface values if you are certain that they contain dynamic values of comparable types.
When handling errors, or during debugging, it is often helpful to
report the dynamic type of an interface value.
For that, we use the fmt
package’s %T
verb:
var w io.Writer fmt.Printf("%T ", w) // "<nil>" w = os.Stdout fmt.Printf("%T ", w) // "*os.File" w = new(bytes.Buffer) fmt.Printf("%T ", w) // "*bytes.Buffer"
Internally, fmt
uses reflection to obtain the name of the
interface’s dynamic type.
We’ll look at reflection in Chapter 12.
A nil interface value, which contains no value at all, is not the same as an interface value containing a pointer that happens to be nil. This subtle distinction creates a trap into which every Go programmer has stumbled.
Consider the program below.
With debug
set to true
, the main function collects the
output of the function f
in a bytes.Buffer
.
const debug = true func main() { var buf *bytes.Buffer if debug { buf = new(bytes.Buffer) // enable collection of output } f(buf) // NOTE: subtly incorrect! if debug { // ...use buf... } } // If out is non-nil, output will be written to it. func f(out io.Writer) { // ...do something... if out != nil { out.Write([]byte("done! ")) } }
We might expect that changing debug
to false
would
disable the collection of the output, but in fact it causes the
program to panic during the out.Write
call:
if out != nil { out.Write([]byte("done! ")) // panic: nil pointer dereference }
When main
calls f
, it assigns a nil pointer of type
*bytes.Buffer
to the out
parameter,
so the dynamic value of out
is nil
.
However, its dynamic type is *bytes.Buffer
, meaning
that out
is a non-nil interface containing a nil pointer
value (Figure 7.5),
so the defensive check out != nil
is still true.
As before, the dynamic dispatch mechanism determines that
(*bytes.Buffer).Write
must be called but this time with a
receiver value that is nil.
For some types, such as *os.File
, nil
is a valid receiver (§6.2.1), but *bytes.Buffer
is not among them.
The method is called, but it panics as it tries to access the buffer.
The problem is that although a nil *bytes.Buffer
pointer has
the methods needed to satisfy the interface, it doesn’t satisfy the
behavioral requirements of the interface.
In particular, the call violates the implicit precondition of
(*bytes.Buffer).Write
that its receiver is not nil,
so assigning the nil pointer to the interface was a mistake.
The solution is to change the type of buf
in main
to
io.Writer
, thereby avoiding the assignment of the dysfunctional
value to the interface in the first place:
var buf io.Writer if debug { buf = new(bytes.Buffer) // enable collection of output } f(buf) // OK
Now that we’ve covered the mechanics of interface values, let’s take a look at some more important interfaces from Go’s standard library. In the next three sections, we’ll see how interfaces are used for sorting, web serving, and error handling.
18.118.145.114