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.