Most programming languages have a feature called “default values”. A default value lets you choose the initial value of a variable, a class field, or a function parameter. If the caller does not provide a value, the default is used.
In TypeScript, default values look like this:
class Person {
constructor(private first_name: string = "John", private last_name: string = "Doe") {}
getName(): string {
return `${this.first_name} ${this.last_name}`
}
}
console.log(new Person().getName()) // "John Doe"
Both constructor parameters have defaults, so new Person() works without any arguments. Without the defaults, TypeScript would not compile that call. In plain JavaScript there is no compiler to stop you, so the same code would print "undefined undefined".
Notice one thing: with default values, you choose the fallback value. Go does this differently.
Zero Values
Go does not let you set default values for variables or struct fields. Instead, Go has “zero values”. Every type has a fixed initial value, chosen by the language. When you declare a variable without giving it a value, it holds the zero value of its type.
This means there is no such thing as an uninitialized variable in Go. This is different from C, where a variable can hold random garbage until you assign a value to it.
| Type | Zero Value |
|---|---|
| bool | false |
| int (and all numeric types) | 0 |
| float64 | 0 |
| string | "" |
| pointer | nil |
| slice | nil |
| map | nil |
| chan | nil |
| func | nil |
| interface | nil |
var b bool
var num int
var str string
var ptr *string // a pointer to any type has a nil zero value
var slice []int // a slice of any element type has a nil zero value
var m map[string]string // a map of any key/value types has a nil zero value
var ch chan int
var fn func()
var iface interface{}
fmt.Printf("bool zero value b = %t\n", b)
fmt.Printf("int zero value num = %d\n", num)
fmt.Printf("string zero value str = %q\n", str)
fmt.Printf("pointer zero value ptr = %v\n", ptr)
fmt.Printf("slice zero value slice == nil is %t\n", slice == nil)
fmt.Printf("map zero value m == nil is %t\n", m == nil)
fmt.Printf("chan zero value ch = %v\n", ch)
fmt.Printf("func zero value fn = %v\n", fn)
fmt.Printf("interface zero value iface = %v\n", iface)chan, map, slice, pointers, functions, and interfaces are reference-like types. Their values point to data stored somewhere else in memory. For these types, nil means “points to nothing yet”.
structs and arrays are value types. They hold their data directly, so their zero value is not nil. The zero value of a struct is a struct where every field has its own zero value. The zero value of an array is an array where every element has the zero value of the element type.
type Person struct {
firstName string
lastName string
}
var p Person
var arr [3]int
fmt.Printf("struct zero value p = %+v\n", p) // {firstName: lastName:}
fmt.Printf("array zero value arr = %v\n", arr) // [0 0 0]Default values for structs
Unlike TypeScript or C#, Go has no built-in way to give default values to struct fields. People have asked for this feature, but the proposals were declined. See, for example, this long discussion. To get default values, the common approach in Go is a NewXxx constructor function:
type Person struct {
firstName string
lastName string
}
func NewPerson() Person {
return Person{firstName: "John", lastName: "Doe"}
}When a struct has many optional fields, the functional options pattern builds on the same idea. The constructor sets the defaults, and callers change only what they need.
The dangers of nil zero values
The nil zero values can cause bugs. Each nil type fails in its own way:
Nil pointers. If you dereference a nil pointer, the program panics: panic: runtime error: invalid memory address or nil pointer dereference. This is not a raw segmentation fault like in C. The Go runtime catches the fault and turns it into a panic. A panic still runs deferred functions, and you can even catch it with recover().
Nil maps. This one surprises almost every Go beginner. Reading from a nil map is safe: it returns the zero value of the value type. But writing to a nil map panics:
var m map[string]string
fmt.Println(m["missing"]) // fine: prints ""
m["key"] = "value" // panic: assignment to entry in nil mapNil channels. Sending to a nil channel, or receiving from one, does not panic. It blocks forever. If every goroutine in the program is blocked, the runtime stops the program with fatal error: all goroutines are asleep - deadlock! (the goroutine dump will show [chan send (nil chan)]). But if other goroutines are still running, the send on the nil channel simply hangs that one goroutine forever. This can be worse than a crash, because nothing tells you it happened.
Nil interfaces. A type assertion on a nil interface panics in its single-value form. v := iface.(int) fails with interface conversion: interface {} is nil, not int. The comma-ok form is safe to use when the interface might be nil:
var iface interface{}
if v, ok := iface.(int); ok {
fmt.Println(v)
} else {
fmt.Println("iface does not hold an int")
}Nil functions. Calling a nil function panics with the same error as a nil pointer.
So which of these do you need to initialize before use? Maps before writing, and channels before sending or receiving:
m := make(map[string]string)
m["key"] = "value" // safe: m was initialized
ch := make(chan int)
go func() {
ch <- 1
}()
fmt.Println(<-ch) // 1Slices are not on that list. A nil slice is normal in Go and almost never needs make. Everything you normally do with a slice also works on a nil slice: len returns 0, range does nothing, and append works too, as we will see next.
Slices: why the nil zero value is safe
The append function adds elements to the end of a slice. When the slice’s backing array is too small for the new elements, append creates a new, larger array and returns a slice that points to it. This is why you must always store the result of append, usually back into the same variable:
var sl []int
fmt.Println("sl == nil:", sl == nil) // true
sl = append(sl, 1, 2, 3, 4)
fmt.Println("sl == nil:", sl == nil) // false
fmt.Println(sl) // [1 2 3 4]This is why a nil slice is safe to use without initialization. append treats a nil slice like an empty one and allocates memory on first use. The zero value just works.
Missing map keys return the zero value
Zero values also appear when you read a missing key from a map. There is no exception and no undefined; you get the zero value of the value type:
counts := map[string]int{}
counts["hello"]++ // works even though "hello" was never set: 0 + 1
fmt.Println(counts["hello"]) // 1
fmt.Println(counts["world"]) // 0, missing key gives the zero valueThis makes counters and sets very short in Go. But it also hides a question: was the value really 0, or was the key missing? When you need to know, use the comma-ok form: v, ok := counts["world"].
Zero value or missing value?
The map question above is a small example of a bigger problem. Zero values have one real weakness: they cannot tell you the difference between “the value is 0” and “no value was given”. This shows up everywhere in JSON APIs:
type User struct {
Age int `json:"age"`
}After unmarshaling, {"age": 0} and {} both give Age == 0. If your API needs to know the difference (for example, a PATCH request where a missing field means “do not change”), the common answer is a pointer field:
type UserPatch struct {
Age *int `json:"age"`
}Now a missing field gives Age == nil and a real zero gives *Age == 0. The database/sql package solves the same problem with types like sql.NullInt64, because SQL NULL is also “missing”, not “zero”.
Make the zero value useful
“Make the zero value useful” is one of the Go proverbs, and the standard library follows it. Many types are designed so their zero value is ready to use, with no constructor needed:
var mu sync.Mutex // ready to Lock, no initialization
mu.Lock()
mu.Unlock()
var sb strings.Builder // ready to write to
sb.WriteString("hello, ")
sb.WriteString("world")
fmt.Println(sb.String()) // hello, world
var buf bytes.Buffer // ready to use as an io.Writer
fmt.Fprintf(&buf, "%d bottles", 99)
fmt.Println(buf.String()) // 99 bottlesThis is the real benefit of zero values. They are not only a fallback for variables you forgot to set. They are also a design tool. When you define your own types, ask if the zero value can be useful. If it can, your users get a type that works the moment they declare it.