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 from other languages and allows some interesting properties of structures in Go. Here is a full playground link.

Why nil matters for built-in structures Link to heading

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 could not read nil members Link to heading

This shows how some basic code would look if you could not read from nil.

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

More readable code not checking nil Link to heading

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 Link to heading

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 Link to heading

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&#39;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 Link to heading

  • 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