Go cannot mark a Config struct field required — how does your team decide which unset fields default and which fail start-up?
answer
- the compiler will never ask for it
- who is awake when the value is wrong
- bounded mistakes default, unbounded ones fail
- a shipped default is a promise to importers
- the postmortem can take a default away
basics
~20 sClassify 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.
solid answer
~50 sSince the compiler will never tell a caller a field is missing, the decision moves into `New`, and it is an operational commitment rather than a style choice. I sort fields into three buckets. Knobs whose wrong value is merely suboptimal — buffer sizes, log intervals, poll periods — get a named default and a doc comment. Fields whose wrong value fails unboundedly or unsafely — endpoints, credentials, concurrency that fans out to a shared downstream — are required and return an error, because a failed deploy is caught by the person who caused it while a silent default fails at 3am for someone who never chose it. Guesses about the environment get a default *and* a loud start-up line. Then the rule is written down for the team, because deciding it per pull request produces an inconsistent surface, and an incident is allowed to promote a field out of the defaulted bucket.
go deeper
Know that Go cannot make a struct field mandatory, so any 'you must set this' rule is enforced by the constructor at run time, not by the compiler.
Be able to implement both outcomes cleanly: a named default for optional fields, and a clear error naming the field for required ones, both decided in one place.
Argue the choice per field from the blast radius of a wrong value, and show how the resolved configuration becomes visible to whoever is on call.
Own the policy: which classes of field may never carry a default, what a shipped default commits you to across teams you cannot redeploy, and how an incident is allowed to change that decision.
## Why the decision cannot be delegated to the language In Go there is no required field. A caller writes `Config{Queue: "jobs"}` and the compiler is content; every field it did not mention has a value. Nor is there an ergonomic way to force a field into the constructor's parameter list once the Config has more than two or three of them. So the only place a missing field can be caught is inside `New`, at run time, and the only person who can decide what happens is the author. That makes 'default or reject' a design decision with operational consequences, not a matter of taste. ## The cost asymmetry Compare the two failure shapes. A **rejected** field fails at start-up. The deploy does not take traffic, the error names the field, and the engineer who introduced the gap is at their desk with the change still in front of them. The cost is friction: more broken rollouts, more ceremony every time a field is added, and a bad error message can burn twenty minutes. A **defaulted** field never fails. It works until the day the default is wrong for one environment, and then it fails as behaviour rather than as an error, at an arbitrary hour, in front of an engineer who does not know the field exists and cannot see that a value was ever chosen. The cost is a low-probability, high-cost, poorly-attributed incident. The rule that falls out: **choose the failure whose cost you can predict.** Where a wrong value is bounded and recoverable, default it and keep the service startable. Where it is unbounded — no deadline, no limit, the wrong endpoint, a fan-out that overloads a shared dependency — take the friction. ## The buckets, concretely - **Required.** Identity and destination (queue, address, credentials); anything that decides where data goes or who it belongs to; anything with a blast radius outside the process. There is no defensible default because the right answer is per deployment. - **Defaulted, quietly.** Internal sizing that only affects this process's own efficiency: buffer sizes, intervals, worker counts for a local-only workload. Document the value in the field comment. - **Defaulted, loudly.** Values that are guesses about the environment and that an operator may need to overrule — concurrency limits against a shared downstream, back-off ceilings. Default so the service runs, but say in the start-up line that the value was defaulted, so the on-call engineer sees a choice nobody made. A useful tiebreaker: ask who is paged when this value is wrong. If it is a team that cannot see your source, it is a candidate for required or loud. ## The extra constraint when you are a library Inside your own service you can change a default and redeploy. In a package other teams import, the default is a promise: every importer who never wrote the field inherits your value, and changing it later alters their runtime behaviour in a release that shows no diff on their side. That pushes libraries toward fewer defaults and more required fields, or toward defaults so conservative that being wrong is cheap. It also means a default change is a release-note event, not a refactor — and if you cannot redeploy the importers, you have effectively frozen the value. ## Making the policy survive people Decided per field, per pull request, this comes out inconsistent: two services in the same team where one refuses to start without a queue name and the other happily consumes from `""`. Write it down once — a short convention listing what is always required, what must appear in the start-up line, and the required shape of a configuration error message — and enforce it in review. A convention costs one page and removes the argument from every future pull request. ## Revisiting after an incident The defaults you ship are a hypothesis about how the service is operated, and incidents are the evidence. The most common correct outcome of a config-caused incident is not 'change the default' but 'this field should never have had one', and the on-call rotation is entitled to force that. Give them somewhere to say so: the postmortem action is a field promoted from defaulted to required, plus a validation rule, plus the start-up echo if it was missing. The action that does not work is 'remember to set it'. ## What separates a principal answer Not a longer list of validation techniques. It is naming the cost asymmetry, attaching it to who gets paged and who can redeploy, treating a shipped default as a commitment to importers, and having a mechanism — a written convention and a postmortem path — rather than a preference.
- What changes when the Config lives in a library other teams import rather than in your own service?You cannot redeploy your callers, so a default becomes a promise. Changing it later alters their behaviour in a release whose diff they never see, which makes it a release-note event. Libraries should carry fewer defaults, more required fields, and defaults conservative enough that being wrong is cheap.
- How do you keep 'fail fast' from making every deploy fragile?Validate everything at construction rather than one field per restart, and report all the failures together instead of stopping at the first. Make each message name the package, the field, the rule and the value seen. Most of the pain of strict configuration is bad error messages, not strictness.
- What do you hand the on-call rotation so they can overrule a default at 3am?The start-up line showing which values are actually in force and which were defaulted, a documented way to set the field in a deployment, and a rollback. What they must not need is a code change, a rebuild, or someone who knows the package to be awake.
- How would you decide this for a field that only matters under load?Treat it as loudly defaulted: pick a conservative value so the service starts, log that it was defaulted, and put a review trigger on it — the first capacity exercise or the first incident that touches it promotes it to explicit. Fields that only bite under load are the ones nobody sets.
saying these in an interview costs you the question
- Defaults every field so the service always starts
- Makes every field required and blames the resulting deploy friction on operators
- Treats a shipped default as an implementation detail rather than a commitment
- Changes a library's default in a patch release without telling callers
- Leaves the decision to whoever writes the next pull request
- Answers an incident with 'remember to set it' instead of a structural change