Zero Values

Every Go type has a zero value, so a declared variable is never uninitialised and never holds leftover memory.

Syntaxvar x T // x holds T's zero value: 0, false, "" or nil

A 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

TypeZero value
numbers, such as int or float640
boolfalse
string"", a string of no characters
pointer, slice, map, channel, function, interfacenil
structevery field set to its own zero value
arrayevery 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

Example · go
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 done

When 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.

Example · go
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.

Example · go
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 released

Telling "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.

Example · go
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

  • Be the first to comment on this lesson.