For Go builtins, nil and empty read the same

An interesting property of Go’s built in data structures is that read operations work even when they are nil! Not only that, but read operations on nil built in structures behave just as if they are non-nil but empty. This is different than other languages and allows some interesting properties of structures in Go. Here is a full playground link: https://play.golang.org/p/KZktHi9Ht7.

Why nil matters for built in structures

Go does not have a way to force constructors for structs. The closest we can get is making the struct private and documenting users to get an instance from a function named New. This means there’s no way to initialize a member variable of a struct before people use it. Any maps, channels, or slices in your struct will start as nil.

What if you couldn’t read nil members

This shows how some basic code would look if you couldn’t read from nil.

type Bag struct {
items []int
}
func (b *Bag) Count() int {
// Checking items for nil is annoying
if b.items == 0 {
return 0
}
return len(b.items)
}

More readable code not checking nil

The annoying part is the need to check that b.items is nil before getting the Count. Lucky for us, read operations like len work great for nil slices and we don’t need to check if it’s nil first.

func (b *Bag) Count() int {
return len(b.items) // len(nil slice) == 0
}

It’s not just slices that work for read operations on nil. Maps work as well.

type Bag struct {
names map[string]struct{}
}
func (b *Bag) Contains(s string) bool {
// No need to check if names is nil
_, exists := b.names[s]
return exists
}
func (b *Bag) Size() int {
return len(b.names)
}
func main() {
// Note never set b.names
var b Bag
b.Size()
b.Contains("name")
}

Notice that users can create a Bag object and don’t need to create a names struct to do read operations like contains or size.

Finally, let’s show how this works with channels too.

type Producer struct {
items chan int
}
func (p *Producer) Item() int {
select {
case i := <- p.items:
return i
default:
return 0
}
}
func (p *Producer) Size() int {
return len(p.items)
}

Reading an empty channel is a blocking operation, so it’s the same for nil! Item() will return zero if the channel is empty, or nil.

Supporting nil in your own operations

For Producer, even though items can be nil, you would still get panics if Producer itself was nil. It’s very reasonable to support nil for your own structures. If you do, a reasonable expected behavior for your own structs is to have nil operate the same as empty for read operations. You could do that with Producer like below.

func (p *Producer) Size() int {
if p == nil {
return 0
}
return len(p.items)
}

Virality of zero struct support

When your structure supports read operations when a zero value, people can embed it inside their own structs and now their zero struct will behave reasonably on read operations as well.

For example, look at some code that contains a Producer.

type Grocery struct {
p Producer
}
func (g *Grocery) Inventory() int {
return p.Size()
}
var g Grocery
fmt.Println(g.Inventory())

Because Producer behaves reasonably when empty, Grocery can too. Similarly, if Producer behaves correctly on nil operations, then Grocery can contain a pointer to Producer, not just Producer itself.

type Grocery struct {
p *Producer
}
func (g *Grocery) Inventory() int {
// This won't panic if Producer.Size() checks nil itself
return p.Size()
}
var g Grocery
fmt.Println(g.Inventory())

Tweet sized advice about nil for library authors

  • For go built in objects, nil and empty read the same.
  • If you want to support nil, it should behave the same as empty
  • Nil and empty struct support is viral
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.