When 'nil' Isn't

James Palawaga
James Palawaga
Software Engineer

One sunny Monday afternoon, our crash reporting alerted us to an error instantly recognizable by any Go programmer.


panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x8 pc=0x100135794]
goroutine 1 [running]:
(*uuid.UUID).String(0x0)
  /app/polytomic/stringer.go:86 +0x24  
...
exit status 2

The code in question was trying to take a uuid.UUID from a database and turn it into a string so that we could sync it elsewhere. I opened up the code that the stacktrace pointed to and it looked (something) like this:


81| func defaultStringer(v interface{}) (string, bool) {
82|    switch val := v.(type) {
83|    case nil:
84|        return '', true
85|    case fmt.Stringer:
86|        return val.String(), true
87|    }
88|
89|    return fmt.Sprintf('%v', val), true
90| }

This is…a little perplexing, isn’t it? We got a nil dereference error, but there’s a nil case on line 83, right before we attempt to String() the value!

Strange, I thought. But perhaps the type isn’t nil, the value is, and perhaps an extra nil check will fix the code.


81| func defaultStringer(v interface{}) (string, bool) {
82|    switch val := v.(type) {
83|    case nil:
84|        return '', true
85|    case fmt.Stringer:
86|        if val != nil {
87|            return val.String(), true
88|        }
88|    }
89|
90|    return fmt.Sprintf('%v', val), true
91| }

And…voila! 

Not. This actually produced the same error. Additionally, other debugging methods also told me the variable is nil: Printf yielded nil and my debugger confirmed the variable was, indeed, nil. So what the hell?

The Briefest Introduction To Interfaces

Understanding what’s going on here involves a cursory knowledge of how Go implements interface{}, and how .(type) switch statements work. It’s really not too complicated, I promise!

In a very basic sense, an interface{} is a struct with two pointers: one pointer to the interface’s type and another pointer to the value. This becomes immediately evident when opening the reflect package source:


// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
  typ  *rtype
  word unsafe.Pointer
}

At this point you can probably reason about how the Go compiler treats a .(type) switch statement: the switch statement can use the interface’s type pointer in order to match on different types.

Let’s look again at the problematic switch statement.


82| switch val := v.(type) {
83| case nil:
84|    return '', true
85| case fmt.Stringer:
86|     if val != nil {
87|         return val.String(), true
88|    }
88| }

Here, v is not actually the nil interface — it is of type uuid.UUID. However, the interface’s value is a nil pointer. This means that when you do type-check, or even if you compare the value to nil, it is not nil. However, when you try to use the variable, Go will try to deference the value. Because the value pointer is nil, you get a nil dereference error.

How did this happen?

You may not have seen a typed-but-nil interface before, and understandably so. They’re not easy to create without either using the reflect package, or playing with the internal representation of Go variables.

In our case, our database driver was using a custom Valuer in order to deserialize a UUID column from the database into a UUID variable. In the case of a nullable column, the driver created a typed interface with a nil value.

In order to preserve the semantics of idiomatic Go, I made a small change: if the value is nil, just return nil, not a nil UUID interface. This fix is straightforward and ensures that we don’t end up with surprising cases in our codebase.

Postscript

So there you have it. In Go, sometimes variables are not as nil as they appear.

I put together a piece of sample code so that you can play with this yourself. You can find it on GitHub.

We are hiring backend engineers (Go experience not required): email us at contact@polytomic.com to learn more!

Go gopher designed by Renee French; used under the CC BY 3.0 license.

Data and RevOps professionals love our newsletter!

Success! You've subscribed to Polytomic's newsletter.
Oops! Something went wrong while submitting the form.