I said nil, not nil

Last week, I was working on extending goworker to use Sentinel, a Redis HA solution, instead of plain Redis. The high level goal was clear enough, but the way there was anything but.

The root problem which made me go through all stages of programming:

  1. that’s easy;
  2. huh, strange;
  3. WTF?;
  4. smashing head agains a wall; and
  5. facepalm/double facepalm/n-facepalm

turned out to be that nil != nil. At least not always.

In this particular instance, I was returnig nil to a pool of Redis connections, when the checked out connection was either dead (= no PONG response) or the instance it was connected to had been demoted to a slave. Exactly as the documentation instructed:

If you no longer need a resource, you will need to call Put(nil) instead of returning the closed resource.

My growing confusion and frustration was reflected in the number of fmt.Println in the code. First in my code, then in the original goworker code, finally in the pool code. I’m returning nil, it gets sent to the channel which effectively serves as a queue, when it is read from the channel, nil check is performed and if the resource is nil a new connection created using the provided factory. The factory branch was, however, never performed. How was this possible? Then, at the same time, I tried to print fmt.Printf("%v\n", wrapper.resource) and a colleague chimed in with a suggestion.

As it turns out, this is a common situation and it’s easy to google for a solution when you know that you nil != nil is your problem. Some claim that it is a common gotcha, but if I ever read about it, I have forgotten it completely. From the top results I read: nil in Golang has really useful examples but Steve Francia explains it bit more clearly.

I don’t want to repeat them, but let me at least summarize: some types in Go have zero values (e.g. empty string, 0, false) and they can’t ever be nil, other can be nil and default to it (maps, slices, channels, functions, pointers). Their nils are however typed so nil function is not the same as nil map. OK, makes sense and you’d hardly ever compare those two. Even then, you’d be fine with the result.

However, there also exists nil type which can be returned by functions with an interface return type or accepted by functions with interface type argument. This nil doesn’t equal slice or pointer nil.

// A normal interface
type Doer interface {
	Do(work string)
}

// A type which implements that interface
type Person struct {
	name string
}

func (p *Person) Do(work string) {
	fmt.Println(p, "does", work)
}

// A function which accepts that interface as an argument
func Do(doer Doer, work string) {
	// and checks against nil
	if nil == doer {
		fmt.Println("Nil argument, nothing gets done!")
	} else { 
		doer.Do(work)
	}
	// var n Doer
	// fmt.Printf("%v %v\n", doer, n)
	// fmt.Printf("%#v %#v\n", doer, n)
}

func main() {
	johny := &Person{"Johny"}
	var jimmy *Person
	
	Do(johny, "the dishes")
	Do(nil, "does nothing")
	Do(jimmy, "the homework")
}

// &{Johny} does the dishes
// Nil argument, nothing gets done!
// <nil> does the homework

Playground

My expectation was that both the second and the third call would print Nil argument, nothing gets done!. If you uncomment the first fmt.Printf you’ll see that doer and nil have the same string representation, so in frustration you take it to mean that they are the same. Uncomment the other fmt.Printf and suddenly, you can see the difference!

n is always <nil>, it is something that could be a Doer. So is doer in the middle call because we call Do(nil) and based on the argument type Doer the compiler figures out that the value should be <nil>. In the last case, however, we declare jimmy which is an empty pointer to a Person, we pass this to Do and because *Person is a Doer, it compiles. But then we compare this Person-shaped nil to Doer-shaped nil and it can never come out true!

The solution to this problem is a mirror image to the one described in the linked article above:

func PersonDo(p *Person, work string) {
    if p == nil {
        Do(nil, work)
    }
}

The only question left is when will someone optimize this code and remove PersonDo function as completely pointless.

We're looking for developers to help us save energy

If you're interested in what we do and you would like to help us save energy, drop us a line at jobs@enectiva.cz.

comments powered by Disqus