technical
Understanding Zero Values in Go
Go initializes every type with a zero value; understanding those defaults helps prevent nil errors and produce more useful APIs.
Declare a variable in Go and never assign to it, and it still has a value. An int is 0, a bool is false, a string is "". The compiler and runtime guarantee this for every type, from int and string to maps, pointers and structs. Go calls it the zero value, and I think the name is apt. Like the written zero Boethius is calculating with in the woodcut above, it is not the absence of a number. It is a number you can work with.
The basic types all have an obvious zero:
var x int
fmt.Println(x) // Output: 0
var y float64
fmt.Println(y) // Output: 0
var z bool
fmt.Println(z) // Output: false
var s string
fmt.Printf("%q\n", s) // Output: ""Most of the time that is exactly what you want. The trouble starts with the handful of types whose zero value is nil.
nil as a zero value
For channels, maps, slices, pointers, functions and interfaces, the zero value is nil. The name has always bothered me a little. nil is Latin for “nothing”, and nothing does not sound like a zero you can calculate with. But Go treats nil as a value like any other zero value. You can read from a nil map, range over a nil slice, and call methods on a nil pointer. That is why the spec calls it a zero value, and so will I.
Reading from a nil map is a good example of how far this goes. A lookup returns the zero value of the element type, len is 0, range runs zero times, and delete is a no-op. A nil map is a legitimate empty map for every operation but one: assignment. Writing to a nil map panics.
Avoiding nil panics
var myMap map[string]int
myMap["foo"] = 1panic: assignment to entry in nil mapThis is the one zero value that bites. myMap is nil, and you cannot assign into a nil map. In a three-line example the fix is to create the map with make before the first write:
myMap := make(map[string]int)
myMap["foo"] = 1 // worksIn real code the nil map is rarely this close to its declaration. It is usually a field in a struct that someone built with a literal, passed around, and wrote to much later. Where you make sure the map exists depends on who creates the value: make at the declaration, a constructor function, or a nil check inside the method that writes. I come back to the last option at the end.
Slices are friendlier. A nil slice is also empty, but append works on it without any setup, because append allocates a new backing array when there is none:
var names []string
fmt.Println(names) // Output: []
fmt.Println(names == nil) // Output: true
names = append(names, "John")
fmt.Println(names) // Output: [John]
fmt.Println(names != nil) // Output: trueThat contrast is the real lesson of this section. append is nil-safe, map assignment is not.
A nil slice and an empty slice print the same with fmt. The %v formatting that Println uses writes [, the elements, and ], and never checks whether the slice is nil. Use %#v to see the difference: []string(nil) versus []string{}.
They are not interchangeable, though. json.Marshal encodes a nil slice as null and an empty slice as []. reflect.DeepEqual, and most test assertion libraries, treat them as different. If an API promises an array, return []string{}, not a nil slice. And when you only want to know whether there is anything in a slice, check len(s) == 0. It handles both cases.
Structs and arrays are never nil. Their zero value is a struct whose fields, or an array whose elements, are all zero values themselves. That sounds safe, and mostly it is, but the fields keep their own zero values. A struct with a map field is a perfectly valid value right up until someone writes to the map:
type Config struct {
Labels map[string]string
}
cfg := Config{}
cfg.Labels["env"] = "prod" // panic: assignment to entry in nil mapEmbrace the zero value
One of Rob Pike’s Go Proverbs is “Make the zero value useful”1.
bytes.Buffer is the standard example. Its documentation says “The zero value for Buffer is an empty buffer ready to use”, and it means it. var b bytes.Buffer gives you a buffer you can write to on the next line. No constructor, nothing to remember. sync.Mutex, sync.Once, sync.WaitGroup and strings.Builder all work the same way.
The trick behind it is to initialize lazily, inside the methods that need it. Pointer receivers make that possible. Here is the Config problem from above, solved so that callers never see it:
type Registry struct {
mu sync.Mutex // the zero value is an unlocked mutex
m map[string]int
}
func (r *Registry) Set(k string, v int) {
r.mu.Lock()
defer r.mu.Unlock()
if r.m == nil { // first write: allocate
r.m = make(map[string]int)
}
r.m[k] = v
}
func (r *Registry) Get(k string) int {
r.mu.Lock()
defer r.mu.Unlock()
return r.m[k] // reading a nil map is fine
}var r Registry is ready to use. The map is created on the first Set, and Get never needs it to exist. Not every type can do this. When a type needs something it cannot create itself, like a database connection or a validated config, the zero value cannot be useful. Give it a constructor and document that the zero value is not meant to be used.
This post covers maps and slices because they are where most zero-value surprises happen. Pointers, functions, channels and interfaces have their own nil behaviour. The interface case in particular, where an interface holding a typed nil pointer is itself not nil, deserves a post of its own.
When I design a type now, I ask what var x MyType should do with no further setup. If the answer is “panic”, I have given my callers a nil, a nothing. If the answer is “work”, I have given them a zero, a value they can calculate with. That is the difference the woodcut is drawing, and it is worth drawing in your own code.
Gopherfest 2015 | Go Proverbs with Rob Pike, at the “Make the zero value useful” proverb. ↩︎