I said nil, not nil
Mar 7, 2017 · 4 minute read · Commentsgolang
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:
- that’s easy;
- huh, strange;
- WTF?;
- smashing head agains a wall; and
- 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 nil
s 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
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.