JD Hancock. The Lonely Vacuum of Space. https://www.flickr.com/photos/jdhancock/8955273266 https://creativecommons.org/licenses/by/2.0/
JD Hancock. The Lonely Vacuum of Space. https://www.flickr.com/photos/jdhancock/8955273266 https://creativecommons.org/licenses/by/2.0/

Nil versus empty slices

No lock was held in the creation of this empty slice

Go can have nil slices and empty slices, and they’re different. What’s up with that?

Regular readers of my blog (a select group if ever there was one) will know by now that a slice is syntactic sugar for a struct with a Data pointer, a Length and a Capacity. There’s a definition of this struct in reflect. It looks like this.

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int

When a slice is nil all the fields of the slice header are empty. If you create a non-nil slice with zero capacity the Data field is populated. Thus you can tell the difference between a nil slice and an empty slice. Which I guess is useful if you want to tell the difference between a slice being empty versus not present. Here’s some code to show how you could check this. (playground)

    var nilSlice []byte
    emptySlice := []byte{}

    fmt.Println(nilSlice == nil)
    fmt.Println(emptySlice == nil)

Here’s another short program that shows the content of the Slice header for various byte slices (on the playground)

package main

import (

func main() {
	printSlice(make([]byte, 7, 43))
	printSlice(make([]byte, 0, 2))
	printSlice(make([]byte, 0, 0))	

func printSlice(s []byte) {
	sh := *(*reflect.SliceHeader)(unsafe.Pointer(&s))
	fmt.Printf("%#v\n", sh)

The output is as follows.

reflect.SliceHeader{Data:0x0, Len:0, Cap:0}
reflect.SliceHeader{Data:0xc0000a2f4b, Len:0, Cap:0}
reflect.SliceHeader{Data:0xc0000a2f4d, Len:7, Cap:43}
reflect.SliceHeader{Data:0xc0000a2f4b, Len:0, Cap:2}
reflect.SliceHeader{Data:0xc0000a2f4b, Len:0, Cap:0}

Note in this case that all the zero capacity slices have the same address for their backing data. This is OK as you can never write to that address via these slices. If you did try to append to the slices you’d exceed the capacity and the backing array would be replaced. You’ll also notice that the address is shared with the capacity 2 slice. This is OK for that same reason: any change to the capacity 2 slice would be irrelevant to the zero-capacity slices, and any change to the zero capacity slices would change the address of their backing array.

Is there any penalty for using an empty slice as opposed to a nil slice? Not really. Although in the empty slice case the runtime needs to provide a data pointer, it uses either an arbitrary address on the stack if the slice does not escape the stack or the address of a specific global variable (runtime.zerobase) in the case it does escape. There’s is a little bit more code run creating the slice in this latter case. Calling into the runtime to get the address to use requires some more instructions than simply setting the Data pointer to nil. But when I tried to measure this overhead it was less than a nanosecond. Nothing to worry about except in the extremest of cases.

I think I prefer nil slices, but on closer inspection it’s crazy to have a preference.