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.