Know Your Nil

Posted on March 29, 2017

In Golang, nil is an interesting value. You may be familiar with Go’s philosophy of making the “zero value” meaningful.

Uninitialized variables and fields are set to the zero value. For example, if you have a uninitialized variable of an integer type, its value will always default to 0. An uninitialized string will be the empty string. Likewise, the zero value for a pointer is nil.

nil pointers works the way you would expect. Trying to access a nil pointer is an error. When comparing values, a nil pointer == nil. This matches the way things work in pretty much any language with nil or null values. So far things seem sane. However, a pointer is not the only type of value that can be nil in Go. Most kinds of values that involve a pointer under the hood can also be nil.

Go sometimes makes clever use of the nil values. For example, a nil slice will work when passed to the len() function. Appending to a nil slice works, too.

len() works on nil maps, too. Testing if the map contains keys works. Of course, it always returns false, but it works. Sadly, assigning to a nil map doesn’t. At least the error message explains the problem: panic: assignment to entry in nil map

Channels can also be nil. Here we start to see some sources of real bugs. Writing to and reading from a nil channel blocks forever. In those cute examples, there is only one goroutine so you do get a useful message: fatal error: all goroutines are asleep - deadlock!, but larger programs might appear to continue working.

Remember when I said that a nil pointer == nil? That’s true for pointers. It’s also true for all the other types I mentioned. But the really devious thing is that it is not always true.

In addition to pointers and structure and maps and slices, Go has a concept called an interface. An variable of an interface type (including the all-encompassing empty interface: interface{}) can hold any value that implements that interface. All values implement the empty interface, because there is nothing that they need to implement. When you cast a value to an interface, it looks like you have the same value.

You don’t.

An interface is actually a fat pointer. It stores a pointer to the value, plus information about the type it points to. As it turns out, the information about the type is actually just another pointer. This is where things get interesting. What if one pointer is nil but not the other? As far as I know, it is impossible to construct an interface value where the type pointer is nil but the data pointer isn’t. This leaves two options: either both pointers are nil, or just the data pointer is. These values are not the same.

Assuming you haven’t already clicked the link above, what do you think this prints? True in both cases, right? Well, since I phrased it that way, you know something tricky is going on, and you are right:

both_nil == nil true
val_nil == nil false

This is dangerous. You can check for if x != nil and then get panic: runtime error: invalid memory address or nil pointer dereference when you try to call a method on x. To make matters worse, both values print out as <nil>!

Although awkward, there are ways around this. It is possible to construct another nil value that will compare as equal to that: val_nil == interface{}((*string)(nil)) (this creates a nil pointer to string, then casts it to interface{}). It’s also possible to check for either kind of nil using reflection.


If you are feeling adventurous, you can inspect the difference between the two values with gdb.

Create a file, nils.go:

and compile it with go build -gcflags '-N' nils.go. The “-gcflags ‘-N’” argument prevents the compiler from optimizing away the variables we want to look at.

Next, run gdb nils.

(gdb) break 10
Breakpoint 1 at 0x401068: file nils.go, line 10.

(gdb) run
Starting program: nils

Thread 1 "nils" hit Breakpoint 1, main.main () at nils.go:10
10		fmt.Println(realnil, halfnil)

(gdb) print realnil
$1 = {_type = 0x0, data = 0x0}

(gdb) print halfnil
$2 = {_type = 0x487be0, data = 0x0}

Here we have proof that in one case, both pointers are nil and in the other, just the data pointer is.