In Go's functional options pattern, what happens when two options in the same New call set the same field?
answer
- the constructor just ranges over a slice
- no duplicate detection anywhere
- assignment, so the later one survives
- defaults before the loop, validation after
- a wrapper's own options must go first
basics
~20 sThe 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.
solid answer
~50 sOptions are applied left to right by the `for _, opt := range opts { opt(c) }` loop inside the constructor, so `New(addr, WithTimeout(2*time.Second), WithTimeout(30*time.Second))` leaves the field at 30 seconds. There is no duplicate detection and no error — each option is an ordinary function call that assigns a field. This has two practical consequences. Defaults must be assigned to the struct *before* the loop, otherwise an option cannot override them. And any wrapper that forwards to the constructor must put its own options *ahead* of the caller's, not after, or it will silently overwrite settings the caller asked for explicitly. That last mistake is the classic bug here: an internal `NewInternal` that appends a 30-second timeout after the caller's options, so the caller's 2 seconds never takes effect and nothing in the code or the logs says so.
code
go · 10 lines// Wrong: the caller's WithTimeout is applied, then overwritten.
func NewInternalBad(addr string, opts ...Option) *Client {
return New(addr, append(opts, WithTimeout(30*time.Second))...)
}
// Right: house defaults first, caller's options last.
func NewInternal(addr string, opts ...Option) *Client {
all := append([]Option{WithTimeout(30 * time.Second)}, opts...)
return New(addr, all...)
}go deeper
Remember that the constructor loops over the option slice in order and each option simply assigns a field, so the last one to write a field decides its value. No error is reported for a repeated option.
Explain the two structural consequences: defaults must be assigned before the loop runs, and a wrapper that adds its own options must place them ahead of the caller's so they behave as defaults rather than overrides.
Diagnose the silent-override bug from a symptom — a request timing out at 30 seconds when the code says 2 — and say which regression test would have caught it and where the fix belongs.
Decide and document the precedence rule for the package family, including which options replace and which accumulate, so importing teams do not each discover it from behaviour.
## The mechanism There is no magic in option ordering. The constructor holds a loop: ```go func New(addr string, opts ...Option) *Client { c := &Client{addr: addr, timeout: 5 * time.Second} for _, opt := range opts { opt(c) } return c } ``` Ranging over a slice visits index 0, 1, 2 and so on, so options run in exactly the order they were written at the call site. Each one performs an assignment. Two assignments to the same field mean the second overwrites the first, the same as `x = 1; x = 2`. So `New(addr, WithTimeout(2*time.Second), WithTimeout(30*time.Second))` yields a client with a 30-second dial timeout. The compiler says nothing — the two calls are distinct expressions producing distinct values — and the constructor says nothing either, because it never inspects what an option touched. ## Consequence one: where defaults go Defaults must be written into the struct before the loop. If you set them after, every option a caller passed is thrown away. If you set them by appending a `defaults()` option to the end of the slice, the same thing happens. The safe order inside the constructor is: build the struct with defaults, apply options, then validate. A related habit worth adopting: keep the defaults in one place. `newDefaults() *Client` or a package-level `defaultTimeout` constant means the value appears once, not scattered through both the constructor and the documentation. ## Consequence two: wrappers and forwarding The bug this leaf is really about shows up when one package wraps another's constructor. Suppose a shared internal package wants every client in the fleet to use its own keep-alive and a longer timeout: ```go func NewInternal(addr string, opts ...Option) *Client { return New(addr, append(opts, WithTimeout(30*time.Second))...) } ``` This compiles, reads plausibly, and is wrong. A caller who writes `NewInternal(addr, WithTimeout(2*time.Second))` gets 30 seconds. Their explicit setting is applied and then immediately overwritten. There is no error and no warning; the only symptom is a request that hangs for 30 seconds when the team expected it to fail fast at 2. The fix is to reverse the precedence. The wrapper's opinions are *defaults*, so they go first and the caller's options go last: ```go func NewInternal(addr string, opts ...Option) *Client { all := append([]Option{WithTimeout(30 * time.Second)}, opts...) return New(addr, all...) } ``` Building a fresh slice rather than appending onto the caller's is also the honest thing to do, since it cannot disturb whatever slice the caller handed you. ## Order matters between *different* fields too Most options are independent, but not all. An option that derives one field from another — say a `WithProfile("low-latency")` that sets a timeout and a backoff window together — is order-sensitive against the individual `WithTimeout`. Whichever runs last wins for the overlapping field. If you ship a composite option like that, document its precedence, or make it set only fields the caller has not already touched, which means tracking whether they were set at all. ## What to do about it as an API author Three defensible positions, and you should pick one deliberately: 1. **Last write wins, documented.** The simplest, and what almost every package does. Say so in the doc comment for the `Option` type: "Options are applied in order; later options override earlier ones." 2. **Detect and reject duplicates.** Possible if options carry an identity — for example, if `Option` is an interface or a struct with a key rather than a bare function. It costs you the simplicity of a function type and is rarely worth it. 3. **Make the composite options additive.** An option that appends to a slice field (an interceptor chain, a list of allowed hosts) does not overwrite, so ordering only decides the sequence, not the survivor. Be explicit about which of your options replace and which accumulate — a caller who expects `WithInterceptor` to accumulate but finds it replaces will lose interceptors silently. ## Testing for it The cheap regression test is to pass the same option twice with different values and assert the second one won, plus a test that a wrapper's defaults are actually overridable by a caller. Two short table tests catch the entire class of ordering bug, and they are the tests reviewers most often forget to ask for.
- How would you let an option refuse to overwrite a value the caller already set?You need to know whether it was set, which a plain field cannot tell you once the default is in it. The usual answers are a parallel set of booleans or a pointer field that is nil until touched, both applied to a temporary struct that the constructor collapses into the real one at the end. It is enough machinery that most packages document last-write-wins instead.
- Should an option that appends to a slice field behave differently from one that assigns?Yes, and the difference must be documented on each option, not left to be discovered. An accumulating option — an interceptor chain, an extra allowed host — makes ordering decide the sequence rather than the winner, and passing it twice keeps both values. A caller who assumes a replacing option accumulates loses a setting silently, which is the same failure in the other direction.
- Where does validation belong relative to the option loop?After it. Validating inside an individual option can only see that option's value, so it cannot catch a combination that is invalid as a whole — a read deadline shorter than the keep-alive interval, say. Running the whole option list first and validating the assembled struct once catches cross-field problems and gives one clear error rather than several partial ones.
Options are stacked like sticky notes on the same line of a form: everyone writes over the previous note, and only the top one is read. Whoever writes last decides, whether or not they meant to.
saying these in an interview costs you the question
- Believing the first option to set a field wins
- Expecting a compile error or a run-time error on a duplicate option
- Applying package defaults after the option loop instead of before
- A wrapper appending its own options after the caller's and calling it a default
- Assuming options are applied concurrently or in an unspecified order