skip to content

Useful Zero Values

A type whose zero value works needs no constructor at all, which is why var buf bytes.Buffer compiles and runs. Knowing when that is impossible is the other half of the answer.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

In Go, what does it mean for a type to have a useful zero value, and why do package authors design for one?

level: juniorimportance: must knowfreq 70%

answer

  1. declaring the variable is enough
  2. no New function to call
  3. sync.Mutex and bytes.Buffer both do it
  4. zero-filled memory as a valid state
  5. nil Transport means the default one

basics

~20 s

A type has a useful zero value when var x T is ready to use with no constructor call. bytes.Buffer, sync.Mutex and http.Client all work this way, so callers can declare or embed them and start calling methods immediately.

solid answer

~50 s

Go zero-fills every declaration, so `var x T` always produces a defined value: numeric fields are 0, strings empty, pointers, slices, maps, channels, funcs and interfaces nil. A type has a *useful* zero value when that state is already a valid, working instance. `var buf bytes.Buffer` is an empty buffer you can write to; `var mu sync.Mutex` is an unlocked mutex; `var wg sync.WaitGroup` has a counter of zero; `var c http.Client` sends requests using `http.DefaultTransport` because a nil `Transport` field means "use the default". Designing for this removes a whole class of caller mistakes — there is no `New` to forget, the type embeds cleanly as a field of a bigger struct with no init code, and a package can hand out a variable rather than a factory. It only works if every member's zero state is meaningful; a map, channel or func member that must be non-nil breaks it and pushes you back to a constructor.

code

go · 17 lines
go
type Recorder struct {
	mu  sync.Mutex
	buf bytes.Buffer
	n   int64
}

func (r *Recorder) Record(line string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.buf.WriteString(line) // zero Buffer is an empty buffer
	r.n++
}

func demo() {
	var r Recorder // no New, no init step
	r.Record("cpu=0.42\n")
}

go deeper

for a junior

Be ready to say that Go zero-fills every declaration and to name two or three types you can use straight after var — a bytes.Buffer, a sync.Mutex, a sync.WaitGroup. Knowing that there are no constructors in Go is the point being checked.

for a middle

Explain the mechanics: which field kinds keep the zero value valid (numbers, strings, slices, embedded mutexes) and which break it (map, channel, func). Be able to show how nil-means-default field semantics, as in http.Client's Transport, buy you a usable zero.

for a senior

Show the production judgment: an unusable zero value fails at run time, not compile time, so it becomes an on-call surprise. Talk about documenting the contract on the type and about choosing representations that keep zero valid rather than adding a constructor by reflex.

for a principal

Own the API-shape tradeoff. A usable zero value is a promise other teams will depend on and you cannot quietly withdraw later, so decide early whether a type is a value people declare or an object they must build, and keep that consistent across the packages you publish.

## The rule Go gives you for free Go has no constructors. Every variable, every struct field and every element of a newly made slice or array is *zero-filled* by the language: numbers become 0, booleans false, strings "", and pointers, slices, maps, channels, function values and interfaces become nil. There is no "uninitialised memory" state and no field initialiser syntax in a struct declaration — you cannot write `count int = 5` in a type definition. So the question every Go API designer faces is not *whether* callers can declare `var x T`, but whether the value they get when they do is any good. **A type has a useful zero value when the all-zeros state is already a valid, working instance of the type.** That is a design property, not a language property. The language guarantees the zeroing; you decide whether zero means something. ## The canonical examples - `bytes.Buffer` — the zero value is an empty buffer, ready to `Write`, `WriteString` and `String`. No `NewBuffer` call is needed, and `bytes.NewBuffer` exists only for the case where you want to start from existing bytes. - `strings.Builder` — same idea for building strings. - `sync.Mutex` and `sync.RWMutex` — the zero value is an unlocked lock, so a struct that embeds one is immediately lockable. - `sync.WaitGroup` — the zero value has a counter of 0. - `sync.Once` — the zero value has not run yet, which is exactly what you want. - `http.Client` — a zero `http.Client` performs requests using `http.DefaultTransport`, because the documented meaning of a nil `Transport` field is "use the default". `http.Server` is similar: a zero `Timeout`-style duration means "no limit". Notice the pattern in the last two: the designer chose field semantics so that *nil means default* and *0 means unset/unlimited*. That is the main lever you have. If a nil pointer field can honestly mean "use the package default" and a `0` duration can honestly mean "no deadline", then a struct full of those fields has a working zero value. ## Why it is worth designing for 1. **Nothing to forget.** A caller cannot skip a constructor that does not exist. In a language where every object needs `new Thing(...)`, forgetting it is a compile error; in Go, forgetting `NewThing()` produces a value that compiles fine and misbehaves at run time. Making zero work removes the trap. 2. **Composition is free.** If `sync.Mutex` needed initialising, every struct that contained one would need its own constructor, and so would every struct containing *that*. Usable zero values stop the constructor requirement from spreading up the type graph. 3. **Declaration sites get simpler.** A package-level `var defaultRecorder Recorder` needs no `init` function; a field in a config-free struct needs no wiring. 4. **It reads like the rest of Go.** `var b bytes.Buffer` next to `var n int` is the idiom experienced Go reviewers expect, and an unnecessary `New` on a type whose zero value would have worked is a common review comment. ## What it costs, and where it stops The pattern is not free and it is not always available. Three member kinds break it: - **A map member.** Reading a nil map is fine (you get the element's zero value and `ok == false`), but *writing* to one panics with `assignment to entry in nil map`. A type that must record into a map cannot simply be declared and used unless it creates the map on first use. - **A channel member.** A nil channel blocks forever on both send and receive, and closing one panics. Worse, this fails *silently* — a goroutine hangs rather than crashing. - **A function member.** Calling a nil func value panics; the type must nil-check it and substitute a default behaviour. A slice member is the friendly exception: a nil slice has length 0, ranges over nothing, and `append` allocates for you, so `var s []int; s = append(s, 1)` is correct. That is why zero-friendly designs often prefer a slice to a map. There is also a documentation duty. Because the caller cannot tell from the type declaration whether zero is usable, the package doc comment must say so — the standard library's phrasing, "The zero value for Buffer is an empty buffer ready to use", is the convention worth copying. When zero is *not* usable, say that too, and give the constructor's name. ## The porting trap Engineers arriving from languages where every object is built by a constructor tend to write a `New` for every type reflexively, and then to treat the struct literal as forbidden. In Go the opposite instinct is the right starting point: try to make `var x T` work, and reach for `New` only when a member genuinely must be allocated, a background goroutine must be started, or a dependency must be supplied. That is a real design decision with a real payoff, not a stylistic preference.

  • Which standard library types are the usual examples people cite, and what does zero mean for each?
    `bytes.Buffer` and `strings.Builder` are empty and writable; `sync.Mutex` and `sync.RWMutex` are unlocked; `sync.WaitGroup` has a counter of 0; `sync.Once` has not fired; `http.Client` uses `http.DefaultTransport` because a nil `Transport` field means "the default". The common thread is that nil and 0 were given a sensible meaning by the designer.
  • Which member types stop you from giving your own struct a usable zero value?
    A map member, because writing to a nil map panics; a channel member, because a nil channel blocks forever in both directions; and a func member, because calling a nil func panics. A slice member is fine — a nil slice has length 0 and `append` allocates for you. Anything mandatory that must be supplied by the caller also rules zero out.
  • How does a caller know whether a type's zero value is usable?
    Only from the doc comment. The convention is to state it on the type — the standard library writes "The zero value for Buffer is an empty buffer ready to use" — and, when zero is not usable, to say so and name the constructor. Nothing in the type declaration itself carries that information.

A useful zero value is a tool that arrives assembled in the box: unpack it and start working, instead of hunting for the setup instructions.

saying these in an interview costs you the question

  • Claims every Go struct needs a New function like a constructor language
  • Says the zero value of a struct is nil
  • Thinks var x T leaves the memory uninitialised or full of garbage
  • Believes bytes.Buffer must be created before you can write to it
  • Assumes a nil map can be written to like an empty one
  • Adds a New that only calls make on nothing and returns the struct
open as a page

Why does a counter struct with a map field panic on first use, and how do you make the type safe to declare?

level: middleimportance: should knowfreq 66%

basics

~20 s

The struct zero value holds a nil map, and writing to a nil map panics with "assignment to entry in nil map". Create the map on first write inside the method, or provide a New that makes it.

open as a page

When a Go type must own a background goroutine and a channel, why can't its zero value be usable, and what do you expose instead?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The zero value channel field is nil, so sends and receives block forever and no goroutine is running to serve them. Export a New that makes the channels and starts the loop, and document that.

open as a page

How do you make first-use initialization inside a zero-usable Go type safe when several goroutines call its methods?

level: middleimportance: nice to knowfreq 34%

basics

~20 s

Do the nil check inside the mutex the type already holds, or use a sync.Once field. Both keep the zero value usable. An unlocked nil check is a data race and can throw away one goroutine's map.

open as a page