skip to content

Config Structs and Defaults

Handing New a Config struct keeps a long parameter list readable and keeps adding a field non-breaking, provided callers use keyed literals. The trap is telling an unset field from a zero one.

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

questions

5

How should a Go constructor New(cfg Config) supply defaults for fields the caller left at zero?

level: juniorimportance: must knowfreq 62%

answer

  1. Go has no default parameter values
  2. cfg inside New is your own copy
  3. one named constant per default
  4. resolve it once, before you build

basics

~20 s

New 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 s

Go 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 lines
go
type 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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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
open as a page

In a Go Config struct, how do you tell a field the caller never set from one deliberately set to zero?

level: middleimportance: must knowfreq 55%

basics

~10 s

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

open as a page

One Config struct feeds both NewWorker and NewPublisher — where should defaulting and validation live?

level: middleimportance: should knowfreq 42%

basics

~20 s

Put both on the Config itself: a withDefaults method returning a filled copy, and a validate method returning an error. Every constructor calls them in that order, once, so validation judges the values that will actually be used.

open as a page

A worker's Config.PollTimeout was left at 0 and reached the client as 'wait forever' — how do you stop that class of bug?

level: seniorimportance: should knowfreq 47%

basics

~20 s

Never let an unresolved zero leave the constructor. Decide per field whether zero means unset, a legal request or an error; never forward a downstream API's sentinel; log the effective settings at start-up and test New with an empty Config.

open as a page

Go cannot mark a Config struct field required — how does your team decide which unset fields default and which fail start-up?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Classify fields by the cost of a wrong guess: safe operational knobs get documented defaults, while anything whose wrong value is unbounded or unsafe fails at construction. Write that rule down once, and revisit it after each incident.

open as a page