skip to content

Value Construction

How a caller gets a working value: a zero value that already works, a New function, a Config struct, or variadic options. Each choice moves cost between the author and the caller.

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

explore

questions

14

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

Why do Go libraries write `New(addr string, opts ...Option)` instead of one parameter per setting?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Because callers pass only the settings they care about. Option is a function type such as func(*Client); each With... helper returns one, and New starts from a struct holding the package defaults and applies the options to it.

open as a page

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%

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.

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

In Go's functional options pattern, what happens when two options in the same New call set the same field?

level: middleimportance: should knowfreq 52%

basics

~20 s

The constructor calls options in the order they appear in the variadic slice, so the last one to touch a field wins and the earlier write is silently lost. Nothing warns you: an option is just a function call.

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

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

Why would you declare a Go Option as `func(*Client) error` rather than `func(*Client)`?

level: seniorimportance: should knowfreq 44%

basics

~10 s

So an option can reject a bad value instead of storing it. The constructor then returns (*Client, error), stops at the first failing option and wraps the error, rather than panicking on caller input.

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

As the author of a shared Go client package, when do you expose a setting as a Config field rather than an Option?

level: principalimportance: should knowfreq 34%

basics

~20 s

Expose a Config field when the value is data operators supply and need to see echoed back, or when several settings must be validated together. Reserve options for behavioural knobs you want opaque, unexported and unreachable after construction.

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

What does a `-benchmem` benchmark reveal about the cost of each functional option passed to a Go constructor?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Roughly one heap allocation per option that captures an argument, plus one for the variadic slice. Each With... helper builds a closure holding its captured value, and that closure escapes into the slice handed to the constructor.

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