Why does `var buf bytes.Buffer` work with no constructor, and which other standard library types are usable at their zero value?
answer
- no allocation needed to start
- zero is a meaningful state here
- an unlocked lock is all-zero bits
- nil slice plus append
- some types still ship a New function
basics
~20 sBecause a bytes.Buffer's all-zero state is already a valid empty buffer: its internal byte slice is nil and writes append into a fresh one. sync.Mutex, sync.RWMutex, sync.WaitGroup, sync.Once, sync.Map, strings.Builder and http.Client are usable at zero too.
solid answer
~50 sA type is usable at its zero value when the all-zero state is a meaningful state for that type's methods. `bytes.Buffer`'s zero value is an empty buffer: the unexported byte slice it appends into is nil, and appending to a nil slice allocates one, so `var buf bytes.Buffer; buf.WriteString("hi")` needs no setup. `sync.Mutex` is the same idea from the other direction — zero is defined to mean *unlocked*, so `var mu sync.Mutex` is a ready lock, which is why Go code declares a mutex as a plain field beside the data it guards. Others in this family are `sync.RWMutex`, `sync.WaitGroup`, `sync.Once`, `sync.Map`, `strings.Builder` and `http.Client`, whose zero value is a working client. Not everything qualifies: `sync.Cond` needs `sync.NewCond` because its `L` locker would otherwise be nil, and `time.Ticker` and `time.Timer` need `time.NewTicker`/`time.NewTimer` because a zero one has no channel and never fires.
code
go · 8 linesvar mu sync.Mutex
var buf bytes.Buffer
mu.Lock()
buf.WriteString("ready")
mu.Unlock()
fmt.Println(buf.Len()) // 5go deeper
Remember the two everyday cases: var buf bytes.Buffer and var mu sync.Mutex are ready to use immediately, so no constructor call is needed for either.
Explain the mechanism rather than the list — a nil internal slice that append grows, and a lock whose zero state is defined as unlocked — and name a type that does need a New function.
Read a type's doc comment for the zero-value sentence before writing a constructor call, and be able to say which fields of a zero http.Client or http.Server are dangerous defaults rather than harmless ones.
Treat this as a review standard: a type your teams embed either documents a usable zero value or is only obtainable through a constructor, and a type that half-works at zero is the one that will surface as a panic in someone else's service.
## What "usable at the zero value" means Go guarantees that a declared variable is zeroed. A library author can exploit that: if you lay out a type so that the all-zero state is a *valid* state for its methods, then callers never need a constructor. `var x T` is enough. This is a deliberate design idiom in the standard library, not an accident. ## Why `bytes.Buffer` qualifies A `bytes.Buffer` accumulates bytes in an unexported slice plus a read offset. At zero, the slice is nil and the offset is 0. Appending to a nil slice is legal in Go and produces a new backing array, so the very first `Write` or `WriteString` works and allocates exactly then. Reading from an empty buffer returns end-of-file rather than misbehaving. Every method therefore has a sensible answer for the zero state, and the documented contract says the zero value is an empty buffer ready to use. ```go var buf bytes.Buffer buf.WriteString("ready") fmt.Println(buf.Len()) // 5 ``` `strings.Builder` follows the same pattern for building strings. ## Why `sync.Mutex` qualifies Here the trick is choosing the encoding so that zero carries meaning. A mutex's state field uses 0 for *unlocked*, so the zeroed struct is an unlocked mutex and `mu.Lock()` is correct on a freshly declared variable. That is why idiomatic Go writes ```go type counter struct { mu sync.Mutex n map[string]int } ``` and the embedded mutex simply works when the enclosing struct is zeroed, with no constructor to remember. `sync.RWMutex`, `sync.WaitGroup` (counter starts at 0) and `sync.Once` (not yet run) are all the same idea. `sync.Map` is a little different — it initialises its internal storage lazily on first use — but the observable contract is the same: the zero value is an empty map ready to use. ## Others worth knowing - **`http.Client`** — the zero value is a usable client; a nil `Transport` field means the package default is used. `http.DefaultClient` is exactly a zero-valued client behind a pointer. Note the sharp edge: its `Timeout` field is zero too, which for that field means *no timeout at all*. - **`http.Server`** — usable at zero for the common case, with a zero `Addr` meaning the default port and zero timeout fields meaning no timeout. - **`bytes.Reader`** — the zero value reads as empty, though you normally build one with `bytes.NewReader`. - **`time.Time`** — the zero value is a real, valid instant (the very beginning of the calendar it uses), not an error. Test for it with `t.IsZero()`, which exists precisely because "no time set" has to be expressed by that value. ## What does *not* qualify - **`sync.Cond`** — it needs a `Locker` to wait on, and at zero that field is nil, so you must build it with `sync.NewCond(&mu)`. - **`time.Ticker` and `time.Timer`** — their channel and the runtime timer behind it are created by `time.NewTicker` and `time.NewTimer`. A zero one has a nil channel and never delivers anything. - **`regexp.Regexp`** — a compiled pattern is the whole point; a zero one has nothing compiled into it. - **`database/sql.DB`** — it is created by `sql.Open`, which sets up the connection pool it manages. When a type's zero value is not usable, the standard library signals that by shipping a `New...` function, and the doc comment says what the constructor is for. The absence of such a function, plus a doc sentence like "the zero value is an empty buffer ready to use", is your cue in the other direction. ## One caveat about calling the methods Most of these types declare their methods on the pointer receiver: it is `(*bytes.Buffer).WriteString`, not `bytes.Buffer.WriteString`. `var buf bytes.Buffer` still works because a local variable is addressable, so `buf.WriteString(...)` is shorthand for `(&buf).WriteString(...)`. The same value stored somewhere unaddressable — an element of a map, for instance — cannot have those methods called on it directly. That is a property of method calls, not of zero values, but it is where the pattern most often trips people who store these types inside other containers. ## Why it matters for you as a caller It tells you when a `New` call is required and when it is noise. Writing `buf := bytes.NewBuffer(nil)` where `var buf bytes.Buffer` would do is harmless but unidiomatic; forgetting `sync.NewCond` is a nil dereference. Reading the type's doc comment for a zero-value sentence is the reliable way to tell the two apart.
- What does the zero value of a sync.Mutex represent?An unlocked mutex. The struct's fields are all zero and zero is defined as the unlocked state, so `var mu sync.Mutex; mu.Lock()` is correct with no initialisation at all. That is why Go code declares a mutex as a plain field next to the data it guards rather than constructing one, and why an embedded mutex works as soon as its enclosing struct is zeroed.
- Name a standard library type whose zero value is NOT usable, and say why.`sync.Cond`: its `L` field is the `Locker` it waits on, and at zero that field is nil, so it must be built with `sync.NewCond(&mu)`. `time.Ticker` and `time.Timer` are the same shape — their channel and the runtime timer behind it are created by `time.NewTicker` and `time.NewTimer`, so a zero one never fires. `regexp.Regexp` has no pattern compiled into it.
- Does `var buf bytes.Buffer` work even though the methods take a pointer receiver?Yes. `buf` is an addressable variable, so `buf.WriteString(...)` is shorthand for `(&buf).WriteString(...)` and the compiler inserts the address-of for you. The same value stored somewhere unaddressable — a map element, say — cannot have a pointer-receiver method called on it directly, which is why these types are usually held in a variable or a struct field.
saying these in an interview costs you the question
- Says every standard library type needs a New function
- Thinks a zero sync.Mutex starts out locked
- Claims bytes.Buffer must be built with a constructor
- Assumes sync.Cond works at its zero value
- Believes a zero time.Ticker will still fire