Most seasoned Go programmers are familiar with the memory representation of an
interface
. In fact, A Tour of Go, the canonical
starting point for new Go programmers, makes sure to describe how interface
values contain a value and a type (for a more
specific description, see Russ Cox’s blog post on how interfaces are
implemented), and even goes so far as to
distinguish between interface
values with nil
underlying
values and nil
interface
values. This is explained in the context of
calling methods on a nil
value, a pattern that Go permits. In theory, a
defensive programmer could check if a pointer receiver was nil
in every method
that accessed a member of the receiver.
type Thing struct {
Member string
}
func (t *Thing) GetMember() string {
if t == nil {
return "nil"
}
return t.Member
}
In practice, it is somewhat rare to do so, in part because if the caller
instantiates the type, they already know if it is nil
or not, and if the
author of the type defines a constructor (e.g. NewThing()
) they can return an
error
alongside a nil
*Thing
if construction fails.
However, the fact that interfaces containing nil
values are themselves not
nil
can lead to somewhat odd behavior. In the following example, the
assignment of t
to i
is setting the underlying type of i
to *Thing
and
the underlying value of i
to nil
.
func main() {
var t *Thing
var i any
fmt.Println(t == nil)
fmt.Println(i == nil)
i = t
fmt.Println(t == nil)
fmt.Println(i == nil)
}
Therefore, prior to assignment the value of i
is nil
, while after assignment
of a nil
value it is not.
true // t pre-assignment
true // i pre-assignment
true // t post-assignment
false // i post-assignment
This scenario is somewhat contrived, and a reasonable case could be made that
this type of explicit assignment makes it clear that we are initializing the
underlying type of the interface
. Upon seeing this, one could be tempted to
declare that “assigning to an interface
initializes it”. Unfortunately, that
would be incorrect. Consider the following slight adjustment to our example.
func main() {
var t any
var i any
fmt.Println(t == nil)
fmt.Println(i == nil)
i = t
fmt.Println(t == nil)
fmt.Println(i == nil)
}
Now t
is itself an interface
, but its value is still nil
. Assigning t
to
i
does not initialize i
. In other words, assigning an interface
to an
interface
is similar to assigning the underlying type and value, which are
both nil
in this case.
true // t pre-assignment
true // i pre-assignment
true // t post-assignment
true // i post-assignment
Though these examples illustrate potential stumbling blocks, they have been
detailed at
length in
posts over the years. However, the most frequent issues with nil
interfaces,
in my experience, arise when nesting functions.
func NewThing() *Thing {
return nil
}
func NewI() any {
return NewThing()
}
func main() {
fmt.Println(NewThing() == nil)
fmt.Println(NewI() == nil)
}
At first glance, one might read the preceding code and think “NewI()
returns
NewThing()
, which always returns nil
. Therefore NewI()
always returns
nil
.” As we know from our previous examples, that is not the case. In fact,
this example mirrors the first, except for that the type declarations are in the
function signatures rather than on variable declarations, making the behavior
less obvious.
true // NewThing() == nil
false // NewI() == nil
Similarly to the second example, if we changed the return type of NewThing()
to any
, the same example would yield the following results.
true // NewThing() == nil
true // NewI() == nil
I generally adhere to the “accept interfaces, return structs” principle (with a
few exceptions), but when recently implementing a net.Listener
, which requires
an Accept() (net.Conn, error)
method, I found myself wrapping a function that
returned a struct pointer that implemented net.Conn
. Fortunately, when calling
Accept()
, we can check the error
return value and a well designed
net.Listener
will return a non-nil error
any time that the net.Conn
return
value (or its underlying value) would be nil
. However, it is somewhat
unfortunate that checking net.Conn == nil
does not give us any indication as
to whether calling a method on the value may result in a panic due to
dereferencing a nil
pointer.
I’ve seen comments online that argue that this is a bug in the language, which I would push back on. Though one may disagree with the design choices, the behavior is well-specified. That being said, relying on programmers to not make mistakes is usually not a recipe for success.