In a Go Config struct, how do you tell a field the caller never set from one deliberately set to zero?
answer
- zero is a value, not a silence
- a pointer type has one extra state
- nil means the caller said nothing
- ptr(0) and nil are different requests
- collapse every pointer once, inside New
basics
~10 sMake the field a pointer, such as *int or *time.Duration. A nil pointer means the caller said nothing, so the constructor's default applies; a non-nil pointer to zero means they really asked for zero.
solid answer
~50 sA non-pointer field cannot express it: Go initialises every omitted struct field to its zero value, so `Config{}` and `Config{Retries: 0}` are the same value. Giving the field a pointer type adds the missing third state — `nil` for unset, a pointer to `0` for a deliberate zero. `New` then resolves each pointer once: `retries := defaultRetries; if cfg.Retries != nil { retries = *cfg.Retries }`, and stores the plain resolved value so nothing downstream handles a pointer. The cost is real: callers cannot take the address of a literal without a `ptr` helper, every read needs a nil check, and `==` on two Configs compares pointer identity rather than the pointed-to values. So use pointers only for the fields that need the distinction. The alternatives are a paired `HasX bool`, a documented sentinel such as -1, or choosing units and semantics so zero *is* the right default.
code
go · 17 linestype Config struct {
// nil means the caller said nothing; a non-nil pointer to 0
// means the caller really asked for zero.
Retries *int
PollTimeout *time.Duration
}
func ptr[T any](v T) *T { return &v }
func New(cfg Config) *Worker {
retries := 3
if cfg.Retries != nil {
retries = *cfg.Retries
}
// Config{Retries: ptr(0)} now means "never retry".
return &Worker{retries: retries}
}go deeper
Know that Go fills every omitted struct field with its zero value, so an unset int and an int set to 0 look identical. Recognising that limit is enough at this level.
Explain the pointer field as the third state, dereference it once in the constructor, and name at least two costs: the nil checks and the fact that == compares addresses.
Show when the distinction is worth paying for and when to redesign the field so zero is the right answer. An interviewer wants to hear that a Config of all-pointers means nobody decided what the fields mean.
Frame optionality as an API commitment: once a field is a pointer, every importer writes helpers around it, and taking it back later changes their code. Be ready to say where you draw that line.
## Why a plain field cannot answer the question In Go every field of a composite literal that you do not mention is set to its type's zero value, and there is no 'undefined' state to observe. `Config{}`, `Config{Retries: 0}` and a `Config` decoded from an input that omitted the field all produce the identical value. So a constructor testing `if cfg.Retries == 0` is not asking 'did the caller set this?' — it is asking 'is this zero?', and it silently answers the first question with the second. For most fields that is fine, because zero is not something anyone would ask for. For the fields where zero is a real request, it is a bug waiting for a caller who wants it. ## The pointer field The standard fix is to widen the field's type so it has one more state than the value it carries: ```go type Config struct { Retries *int } ``` Now there are three cases: `nil` (unset — the constructor's default wins), a pointer to `0` (an explicit zero — never retry), and a pointer to anything else. The pointer is not there to be shared or mutated; it is there purely as an optionality marker. Callers cannot write `&0`, because a constant is not addressable, so packages usually export a tiny generic helper — `func ptr[T any](v T) *T { return &v }` — or the caller declares a variable and takes its address. Whichever you pick, document it in the field comment, because a caller staring at `*int` will otherwise guess. ## Resolve once, at the boundary The pointer should live only in the `Config` and only until `New` runs. Inside the constructor each optional field collapses to a plain value: ```go retries := defaultRetries if cfg.Retries != nil { retries = *cfg.Retries } ``` Store `retries`, not `cfg.Retries`. If the pointer survives into the object's fields, every method that reads it needs its own nil check, one of them will eventually forget, and you have converted a defaulting problem into a nil-dereference panic somewhere in the hot path. Resolving at the boundary is the same discipline as parsing input once at the edge. ## What the pointer costs - **Ergonomics.** `Config{Retries: ptr(0)}` is noisier than `Config{Retries: 0}`, and a caller who forgets the helper writes a compile error rather than a typo. - **Allocation.** Each set field escapes to the heap. At construction time that is irrelevant; it only matters if you build Configs in a loop, which you do not. - **Comparison.** A struct with pointer fields is still comparable with `==`, but the comparison is on pointer identity. Two Configs whose `Retries` point at distinct ints both holding 5 are **not** equal. Tests that compare Configs directly will surprise you; compare the resolved values instead. - **Aliasing.** Two Configs built from one helper call share the pointee. Nobody should mutate through a config pointer, but nothing stops them. Because of that, use pointers surgically. A Config where every field is a pointer is a smell: it says the author never decided what any of the fields mean. ## The alternatives, and when each wins - **Design zero to be the default.** By far the best outcome. If `PollTimeout == 0` should mean 'use 30s' and no caller ever wants a zero poll, a plain `time.Duration` is correct and needs no ceremony. Pick field semantics — 'Disabled' rather than 'Enabled', 'ExtraWorkers' rather than 'Workers' — so the useful state is the zero state. - **A paired boolean.** `Retries int` plus `RetriesSet bool` keeps the field comparable and allocation-free, at the cost of two fields that can disagree. Fine inside a package, poor as an exported surface. - **A documented sentinel.** `-1 means unlimited` is compact and used widely in the standard library's own APIs, but it only works when an out-of-range value exists and the documentation is actually read. - **A small wrapper type** holding a value and a `Valid` flag: self-describing, comparable by value, and a good choice when many fields need optionality. - **Make it required.** If there is no defensible default, drop the optionality and reject the missing field in `New`. ## The interview point The candidate to hire names the zero-value problem before naming the pointer, resolves the pointer exactly once in the constructor, and can list what the pointer costs — especially that `==` on the Config now compares addresses.
- What does a pointer config field cost you?An allocation per set field, a nil check at every read, a helper because callers cannot take the address of a literal, and a surprise from `==`: two Configs whose pointers address distinct ints of equal value compare unequal, because struct equality compares the pointers themselves.
- How do you keep a *bool config field from causing a nil dereference later?Dereference it exactly once, in the constructor, into a plain bool field on the object. If the pointer never escapes the Config, no method downstream can forget the nil check. The pointer is an input-format detail, not a runtime representation.
- When would you avoid pointer fields altogether?When you can pick semantics that make the zero value the desired default — naming a field Disabled rather than Enabled, or ExtraWorkers rather than Workers. That removes the question instead of answering it, and keeps the Config comparable, allocation-free and easy to write in a literal.
saying these in an interview costs you the question
- Claims a zero field always means the caller omitted it
- Makes every Config field a pointer by default
- Stores the pointer on the object and nil-checks it in each method
- Expects == on two Configs to compare pointed-to values
- Mutates through a config pointer after construction