Know Your Nil
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.
package main
import "fmt"
func main() {
var both_nil interface{} = nil
fmt.Println("both_nil == nil", both_nil == nil)
var nil_ptr *string
var val_nil interface{} = nil_ptr
fmt.Println("val_nil == nil", val_nil == nil)
}
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.
GDB
If you are feeling adventurous, you can inspect the difference between the two values with gdb
.
Create a file, nils.go
:
package main
import "fmt"
func main() {
var realnil interface{} = nil
var ptr *string = nil
var halfnil interface{} = ptr
fmt.Println(realnil, halfnil)
}
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.