Go is weird: When is a Go slice empty?

Let a be a Go slice. Under what conditions is the following statement true: len(a) == 0?

Unsurprisingly, when the slice is empty, its length is zero:

a := []int{}
len(a) == 0 

But, possibly unexpectedly, the length of a slice is zero even when the slice is nil:

var a []int
len(a) == 0

On some level, this makes sense:

  • What else should len(a) return for a nil slice?
  • Having len(a) fail (e.g. panic) for a nil slice seems wrong.
  • Returning a length and an error is also bad.

Yet, having len(nil) == 0 can be dicey. Imagine a function that returns a slice of matching elements — for example, resulting from a database query. Its signature is surely:

func findElements() ( []int, error )

The error is only not nil if there is a database error, or a comparable condition. But what should the function return if it didn’t find any elements? There are two options:

return nil, nil

or

return []int{}, nil

One can argue that returning an empty (not nil) slice is correct: the set of matching elements is empty; not nil.

How should the calling code check whether any elements were found? Again, there are two options:

res, err = findElements()
if res == nil { ... }

or

res, err = findElements()
if len(res) == 0 { ... }

I’d argue that, semantically, the second form is correct: after all, the result set is empty, not invalid. Note that Go’s convention of having len(nil) == 0 lets the second form of the conditional be semantically correct, either way.

But the first form of the conditional is a problem, because it will only be true if findElements() returns nil, not when it returns an empty slice.

It took me a full day to track down a problem that resulted from this difference…

As an aside: if we don’t need to check whether any elements are found, but merely need to process each, then either return value is fine, because we can range over a nil slice, as well as over an empty slice.

More on nil slices

Turns out, nil slices in Go behave a lot like empty slices. Let a be a nil slice. Then, the following operations are allowed:

  • Take the length: len(a) == 0
  • Append: a = append(a, v)
  • Range over it: for i, b := range a { ... }

The only regard in which a nil slice is different from an empty slice is that a == nil is only true for nil slice, but false for an empty slice.

Given this behavior, I regard nil slices as a bit of a mis-feature: they don’t seem necessary, yet they do introduce a certain amount of ambiguity and uncertainty, as discussed above.

I have seen advice suggesting that a function such as findElements() should return nil in place of an empty result set, with the justification that this is “more idiomatic Go”. That strikes me as a case of “if you can’t fix it, feature it” — just because the language provides nil values, it is idiomatic to use them? I don’t think so. An empty result set is just that: empty. Not nil.

More on nil

In fact, I wonder whether having nil at all is a mistake.

I have not thought about this deeply, but the only reason that mandates the existence of a nil value seems to be as default value for pointer types. I never understood why Go brought back “pointers”, which can point anywhere (including nil) as opposed to “references”, which must refer to a legal value at all times. (In particular since Go’s pointers aren’t even pointers, but references: you can’t do pointer arithmetic on them.)

Not that these thoughts are new: Tony Hoare famously called the invention of Null References: The Billion Dollar Mistake.

An Internet search on that headline will turn up quite a few discussions on the topic, but no real resolution, either.

I do admit that signaling “missing” data is frequently necessary (just look at database NULLs), and nobody seems to have really found a fully satisfactory solution. I have not worked with languages that natively support Option Types, but they remind me awfully of Go’s (dreaded) idiom of returning a value and an error (with the value only being valid if the error is nil). I’d just like to see the use of “missing” data reduced as much as possible, and made explicit, whenever it occurs.