How should a Go constructor New(cfg Config) supply defaults for fields the caller left at zero?
answer
- Go has no default parameter values
- cfg inside New is your own copy
- one named constant per default
- resolve it once, before you build
basics
~20 sNew takes the Config by value, replaces each field still at its zero value with a named package default, and builds the object from that copy. The caller's struct is untouched, and every caller gets identical defaults.
solid answer
~50 sGo has no constructors and no default parameter values, so a `Config` struct plus a `New` function is the idiom that fills the gap. `New` takes the Config **by value**, so `cfg` inside `New` is its own copy and can be assigned to without the caller ever seeing it. For each optional field I test the zero value against an unexported package-level constant: `if cfg.Workers == 0 { cfg.Workers = defaultWorkers }`, so the default is written down in exactly one place. Required fields are checked in the same pass and return an error rather than being guessed at. The constructed object then stores that resolved copy, so nothing downstream re-derives a default or re-reads the caller's struct. This pattern only works where zero is not a legal setting for the field; where zero is legal, the field needs another way to carry the unset/zero distinction.
code
go · 23 linestype Config struct {
Queue string
Workers int
PollTimeout time.Duration
}
const (
defaultWorkers = 4
defaultPollTimeout = 30 * time.Second
)
func New(cfg Config) (*Worker, error) {
if cfg.Queue == "" {
return nil, errors.New("config: Queue is required")
}
if cfg.Workers == 0 {
cfg.Workers = defaultWorkers // cfg is New's own copy
}
if cfg.PollTimeout == 0 {
cfg.PollTimeout = defaultPollTimeout
}
return &Worker{cfg: cfg}, nil
}go deeper
Be ready to write the constructor from memory: take Config by value, reject required fields that are empty, replace zero fields with named constants, then build. Say out loud that Go has no default parameter values.
Explain why the parameter copy matters and why the resolution happens exactly once. An interviewer expects you to name the case the == 0 test cannot handle: a field where zero is a legal request.
Show the judgment about which fields may be defaulted at all and which must fail construction, and how a caller finds out what applied. Defaults you cannot justify are the ones that cause incidents.
Own the fact that a default you ship is a decision every importer inherits. Be ready to say how your team writes those choices down and what it costs to change one after release.
## The gap this idiom fills Go deliberately has no constructors, no named arguments and no default parameter values. A function either takes a parameter or it does not, and a struct literal always produces a fully initialised value: every field you omit is set to its type's zero value (`0`, `""`, `false`, `nil`). So `Config{Queue: "jobs"}` is not a partially built object with holes in it — it is a complete `Config` in which `Workers` is `0` and `PollTimeout` is `0`, indistinguishable from a caller who typed those zeros out. That is why the community settled on the pair: an exported `Config` struct the caller fills in as far as it cares to, and a `New(cfg Config) (*T, error)` function that turns it into a usable object. `New` is the one place that knows what a missing field should become. ## Take the Config by value A struct parameter in Go is copied. Inside `New`, `cfg` is a private copy, so writing `cfg.Workers = defaultWorkers` mutates nothing the caller can observe. That matters more than it looks: callers often build one `Config` and hand it to two constructors, or keep it around to log it. If `New` took `*Config` and wrote through the pointer, the second constructor would see the first one's defaults already baked in, and two goroutines constructing from a shared `Config` would be writing to the same memory. Take it by value unless the struct is genuinely large, and even then prefer clarity. ## Resolve, then build The body of `New` reads as three passes, in this order: 1. **Reject what you cannot invent.** `Queue` has no sensible default; an empty one is a programming error, so return an error naming the field. 2. **Fill what you can.** Each optional field gets `if it is zero, use the package constant`. 3. **Construct from the resolved copy.** Store `cfg` in the object, or copy the resolved fields into unexported fields. After `New` returns, nothing else in the package should ask "was this set?" again. A method that writes `if w.pollTimeout == 0 { w.pollTimeout = 30 * time.Second }` has duplicated the policy, and the two copies will drift. ## Name your defaults Write defaults as unexported package-level constants (`defaultWorkers`, `defaultPollTimeout`) declared next to the `Config`. Three reasons: a reader finds all of them in one place; a test can assert against the constant rather than re-typing the literal; and changing one is a one-line diff rather than a hunt through the file. Document each one in the field's doc comment — "Workers is the number of concurrent consumers. If zero, 4 is used." — because that comment is what shows up in the generated docs a caller actually reads. ## The limit of the `== 0` test This whole pattern rests on an assumption: for this field, zero is not a value anybody would deliberately ask for. That holds for `Workers` (zero workers is not a running worker) and for a `PollTimeout` where zero would mean "spin". It does **not** hold for something like a retry count, where `0` is a perfectly reasonable request meaning "never retry" — and it fails badly for any field where zero silently means "unlimited" or "forever" further down the stack. When zero is legal, the field must carry the distinction some other way, typically by becoming a pointer whose `nil` means "unset". Choosing the semantics so that the zero value is also the sane default is the cheapest way out and worth designing for. ## What an interviewer is checking That you know Go gives you no defaulting mechanism and that the constructor is where you supply one; that you understand a struct parameter is a copy; that you can tell a defaultable field from a required one; and that you resolve the configuration exactly once instead of sprinkling `if == 0` through the rest of the package.
- Why take the Config by value rather than as *Config?Because the copy makes `New` free to overwrite fields without side effects. A caller can reuse the same `Config` for a second constructor, log it afterwards, or build from it in two goroutines, and none of that changes meaning. A `*Config` would leak the applied defaults back and turn a shared literal into shared mutable state.
- What breaks if zero is a legal setting for one of the fields?The `== 0` test can no longer tell 'the caller said nothing' from 'the caller asked for zero', so a deliberate zero is silently replaced by the default. That field needs a pointer whose nil means unset, a separate boolean, or a redesign so zero and the default coincide.
- Where do the default values themselves belong?In unexported package-level constants declared beside the Config, and named in each field's doc comment. That gives one place to read them, one place to change them, and something a test can assert against instead of re-typing the literal.
saying these in an interview costs you the question
- Says every caller should just fill in every field
- Writes through a *Config so defaults leak back to the caller
- Scatters magic literals inline instead of named default constants
- Re-applies the default in methods after construction
- Assumes a field's zero value is always a safe default