Would you put a *slog.Logger in context.Context, or put the ids there and let the handler read them?
answer
- two things you could stash in ctx
- one needs every call site to fetch
- the other needs one handler install
- a missing logger becomes a crash
- ids are useful beyond logging
basics
~20 sPrefer small identity values in the context, stamped by a slog.Handler: one install point, and every logger over that handler correlates. A logger in the context needs every call site to fetch it, and a missing one is nil.
solid answer
~50 sBoth designs are used, and the difference is where the obligation sits. Ids in the context plus a context-reading `slog.Handler` means one install point: any logger over that handler stamps the field, including the package default, and call sites only owe you the Context-taking log methods. A `*slog.Logger` in the context means every call site must fetch it through an accessor, which must return `slog.Default()` rather than nil, and the logger becomes an invisible dependency that no function signature admits to. Its advantage is that intermediate layers can enrich what they log without your handler knowing the field exists. My default is ids in the context for cross-cutting identity — run id, request id, customer id — because those values are wanted by more than the logger, and an explicit `*slog.Logger` parameter or struct field for component-level configuration.
code
go · 5 lines// A: identity in the context, the handler stamps it
logger.InfoContext(ctx, "batch committed", "rows", n)
// B: the logger itself in the context
loggerFrom(ctx).InfoContext(ctx, "batch committed", "rows", n)go deeper
Know both options exist: a small identity value in the context that a handler reads, or the logger object itself stored in the context and fetched again by the code that logs.
Explain the mechanics that separate them: a handler install point versus an accessor at every call site, and why the accessor must have a fallback.
Argue the choice with failure modes, not taste: a silently missing field on one side, a hidden dependency and a nil logger on the other, and which obligation your codebase can actually keep.
Own the boundary rule: what is allowed to ride in a context at all, which identity fields are standard across services, and how a component declares its logger as a dependency instead of smuggling it.
## Two designs that look alike Both of these get a run id onto every line without threading a parameter through the call tree: **A. Identity in the context, a handler stamps it.** At the top of the run, `ctx = context.WithValue(ctx, runKey{}, id)`. A wrapping `slog.Handler` reads it in `Handle(ctx, record)` and adds the attribute. Call sites log with `InfoContext(ctx, ...)` and know nothing about correlation. **B. A logger in the context.** At the top of the run, a logger already carrying the run id is stored in the context. Call sites fetch it — `loggerFrom(ctx).InfoContext(ctx, ...)` — and log through it. They produce similar output. They fail differently, and that is what an interviewer is after. ## What design A buys - **One install point.** The handler is constructed once and set as the process default. Every logger built over it stamps the field, including the package-level `slog.InfoContext` shorthand, so components that were never handed a logger still correlate. - **The value is useful to more than the logger.** A run id in the context is also what you put in an error message, a metric label, the final summary line, and the id you quote to support. A logger in the context is only good for logging. - **Nothing to fetch, nothing to be nil.** Call sites use the logger they already have. The only obligation is passing `ctx` to the log call. - **Cheap to reason about.** The set of correlation fields is fixed and visible in one file. Its limits: only the fields your handler knows about are stamped, and a call site that uses the non-context method gets nothing. Intermediate layers cannot contribute a field of their own without touching the handler or the context key set. ## What design B buys - **Layered enrichment.** A middle layer can put a logger carrying `batch=17` into the context it passes down, and everything below picks it up without the handler knowing that `batch` exists. This is the real argument for B, and on deeply layered code it is a genuine one. - **Configuration travels with the work.** A run flagged for verbose output can be given a differently configured logger for its subtree, rather than a global level change. Its costs: - **Every call site fetches.** `loggerFrom(ctx).InfoContext(ctx, msg)` at every line is noisier than `logger.InfoContext(ctx, msg)`, and it is a rule people forget. - **The accessor is a nil-pointer landmine.** A context that never carried a logger yields a nil `*slog.Logger` from a comma-ok assertion, and the next method call panics. The accessor must be total — return `slog.Default()` when nothing is found. - **It hides a dependency.** A function that takes only `ctx` and secretly requires a logger to have been installed in it has an undeclared precondition. That is exactly the criticism levelled at context values generally, and a logger is a much bigger thing to smuggle than a string id. - **Libraries cannot rely on it.** Code outside your module has no idea about your context key, so it will not find your logger. A handler, by contrast, applies to anything logging through the default logger. ## How to choose A workable rule: 1. **Cross-cutting identity goes in the context as small values**, stamped by a handler: run id, request id, customer id, batch number. Small, comparable, useful beyond logging. 2. **The logger itself is a dependency**, so pass it explicitly — as a constructor argument or a struct field on the component — where a component needs its own configuration. 3. **Reach for a logger in the context only when layered enrichment is a real requirement**, and then make the accessor total and write it once in one place, not inline at every call site. ## The tell in an interview The weak answer is "put the logger in the context, it's convenient", with no account of what happens when it is not there. The strong answer names the failure mode of each design — a silently missing field for A, a nil logger and a hidden dependency for B — and picks based on who bears the obligation: one handler author, or every call site in the codebase.
- What is the strongest argument for keeping a logger in the context rather than ids?Layered enrichment. An intermediate layer can place a logger carrying its own field — a batch number, a shard — into the context it passes down, and everything below inherits it without the handler or the context key set knowing that field exists. On deeply layered code that is real value; on flat code it is mostly ceremony.
- If you do store a logger in the context, what must the accessor do?Never return nil. Use the comma-ok assertion and fall back to `slog.Default()` when the context carries nothing, so a lost context degrades to an uncorrelated line instead of a nil-pointer panic that kills the process. Write it once as a package-level helper rather than inlining the lookup at call sites.
- Why does putting the id rather than the logger in the context help beyond logging?Because the id is wanted elsewhere: in the error you return to the caller, in the summary you print at the end of a run, in the value you quote to support so they can grep a day of output. A logger in the context serves only the logging path; an identity value serves every consumer that has the context.
saying these in an interview costs you the question
- Stores the logger in the context and returns nil when it is absent
- Puts a whole request or config object in the context for convenience
- Claims a library will find your context-stored logger
- Argues one design is universally correct with no failure analysis
- Fetches the logger from the context inline at every call site