Zero Values
Every Go type has a zero value, so a declared variable is never uninitialised and never holds leftover memory.
var x T // x holds T's zero value: 0, false, "" or nilA variable has to start out holding something. In Go, a variable declared without a value holds its type's zero value. Every type has one. This is a language guarantee, not a convention, so Go has no uninitialised variables and no variable that holds leftover rubbish from memory. A whole family of bugs simply cannot happen.
What the zero values are
| Type | Zero value |
|---|---|
numbers, such as int or float64 | 0 |
bool | false |
string | "", a string of no characters |
| pointer, slice, map, channel, function, interface | nil |
| struct | every field set to its own zero value |
| array | every element set to its own zero value |
nil means "nothing here yet". Those six kinds of type are each covered in their own later lesson; the point for now is that none of them starts as garbage.
The zero value is meant to be used
Go picks zero values so that a fresh variable is already a sensible starting point. A counter starts at 0 and can be added to at once. Standard library types follow the same rule: a sync.Mutex, which is a lock used in later lessons, works the moment it is declared, with no setup call.
What trips people up
A zero value is indistinguishable from a deliberate zero. A struct field holding 0 might mean "zero" or "nobody filled this in". When that difference matters, you must record it some other way.
Example
package main
import "fmt"
func main() {
var count int
var ratio float64
var ok bool
var label string
// %q puts quotes round a string, so an empty one is visible.
fmt.Printf("%d %g %t %q\n", count, ratio, ok, label)
// A pointer holds the address of a value. Its zero value is nil,
// meaning it holds no address at all.
var p *int
fmt.Println(p == nil)
// Because the zero values are usable, there is no setup step first.
count = count + 1
label = label + "done"
fmt.Println(count, label)
}
// Output:
// 0 0 false ""
// true
// 1 doneWhen to use it
- Declaring a counter or total and adding to it immediately, with no initialisation line.
- Filling in two fields of a config struct and letting the rest default.
- Designing a type whose plain var declaration is already usable, with no constructor.
- Deciding whether a 0 arriving in a request means zero or means the field was omitted.
More examples
A struct's fields are zeroed one by one
Declaring a config struct and then filling in only the fields you care about is everyday Go. It is safe precisely because the fields you skip are zeroed rather than undefined. Add a field to the struct later and old code still compiles, with the new field at its zero value.
package main
import "fmt"
// A struct groups named fields into a single value.
type Config struct {
Host string
Port int
Verbose bool
}
func main() {
var c Config
// Each field holds its own type's zero value.
fmt.Printf("%q %d %t\n", c.Host, c.Port, c.Verbose)
// %+v prints a struct with its field names.
fmt.Printf("%+v\n", c)
}
// Output:
// "" 0 false
// {Host: Port:0 Verbose:false}Types designed so the zero value already works
When you design your own types, aim for this: a plain var declaration should produce something usable, with no required setup function. It removes a step the caller can forget. Note that sync.Mutex is deliberately not copied anywhere here, which later concurrency lessons explain.
package main
import (
"fmt"
"strings"
"sync"
)
func main() {
// A strings.Builder collects text efficiently. The zero value is
// an empty builder, ready to write to.
var b strings.Builder
b.WriteString("ready")
fmt.Println(b.String())
// A sync.Mutex is a lock. Its zero value is an unlocked lock.
var mu sync.Mutex
mu.Lock()
mu.Unlock()
fmt.Println("lock taken and released")
}
// Output:
// ready
// lock taken and releasedTelling "zero" apart from "never set"
This comes up the moment you decode JSON sent by someone else, or write a partial-update endpoint. If 0, "" or false are meaningful values in your data, a plain field cannot record absence, and a pointer field or a separate bool can.
type Settings struct {
// 0 could mean no retries, or could mean the caller said nothing.
Retries int
}
type PatchSettings struct {
// A pointer is nil when the field was absent, and points at a
// number when it was present, including when that number is 0.
Retries *int
}
Discussion