Evan Moses

My blog and projects

A Go Puzzle: How can this panic?

Here’s a puzzle for Go developers. How can the following code possibly panic with a nil pointer dereference?

var someThing *SomeStruct

// various things happen, including possibly assigning to someThing

if someThing != nil && someThing.Body != nil {
    fmt.Printf("%s", someThing.Body)
}

Think about it for a minute. I’ll wait.

OK are you done? I don’t want to spoil it for you.

The Answer

Here’s the definition of SomeStruct,

type SomeStruct struct {
    *http.Response
    OtherField string
}

Now we can see the problem: we checked if someThing was nil but we implicitly referenced a field on the embedded http.Response without checking if Response was nil. If Response is nil is the code will panic.

Think really hard before you embed a pointer

I came across this struct in a API client library that I won’t name. I think it was probably a bad design idea. The struct represented the response from that API. So it looked something like this:

type APIResponse struct {
    *http.Response
    // non-exported fields
}

func (*APIResponse) NextPage() (*APIResponse, error) {...}
func (*APIResponse) HasNextPage() bool {...}

So: it wraps the actual API response, plus some extra methods for dealing with pagination. I can see why the author thought embedding the response was a good idea: it lets the consumer treat it like a normal http response with some extra features.

However, it becomes a problem when you can get a non-nil APIResponse with a nil Response. If you’re going to embed a pointer in a Go struct, you should do you best to make sure you never leave that pointer nil.

All in all I think this API would have been better served by just making the http response a normal struct field, and possibly copying important fields like Status into the struct1:

type APIResponse struct {
    Response *http.Response
    // And if it's really important to you that people type APIResponse.Status
    // instead of APIResponse.Response.Status:
    Status int
    // non-exported fields
}

  1. Of course, if the response is nil, I suppose you leave Status as the zero value and let consumers figure out what do do with http response code 0. ↩︎