What does slog.SetDefault change, and where do slog.Info records go before it is called?
answer
- two ways in: your logger, or slog.Info
- the package functions ask someone for a logger
- one global pointer, set once in main
- unset means the log package's plain stderr line
- records emitted before that line look different
basics
~20 sslog.SetDefault installs a *slog.Logger as the process-wide default, so the package-level slog.Info, slog.Warn and slog.Error use its handler. Before that call, those functions use a built-in handler that prints plain, unstructured lines to standard error.
solid answer
~40 sThere are two ways to log with `log/slog`: through a `*slog.Logger` you built yourself with `slog.New(handler)`, or through the package-level functions `slog.Info`, `slog.Warn` and so on, which delegate to whatever logger `slog.Default()` returns. `slog.SetDefault(logger)` is what swaps that process-wide default, so a single line in `main` decides where every package-level call in the binary — including calls inside dependencies — ends up. Until it runs, `slog.Default()` is a logger over a built-in handler that goes through the standard `log` package's default logger: plain text on stderr, roughly `2026/09/01 10:00:00 INFO starting port=8080`, not JSON and not key-sorted structure. That is why the usual shape is `slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))` as one of the first statements of `main`.
code
go · 8 lines// Before: the built-in default handler, plain text on stderr.
slog.Info("starting", "port", 8080)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
// After: one JSON object per record, on stdout.
slog.Info("starting", "port", 8080)go deeper
Be ready to name the three calls and what each one does: slog.New builds a logger over a handler, slog.SetDefault installs one as the process default, slog.Default returns it. Say out loud that the unset default is plain text on standard error.
Explain the delegation: the package-level functions look up slog.Default() on every call, so replacing the default changes them but not loggers already constructed. Mention that ordering in main decides which records get your format.
Show the operational consequence — records emitted during package init or by an imported package before SetDefault runs land in a different format and often a different stream, which is what breaks a collector's parser at startup. Say where you put the call and why.
Own the rule that only main sets the default and libraries take a logger parameter. Be ready to justify why one process-wide default is worth having at all, given that dependencies you do not control can only reach slog.Default().
## Two entry points, one switch The `log/slog` package gives you two ways to emit a record. 1. **An explicit logger.** `slog.New(h)` wraps a `slog.Handler` — the thing that actually formats a record and writes it somewhere — and returns a `*slog.Logger`. You call `logger.Info("msg", "key", value)` and it goes to that handler, full stop. 2. **The package-level functions.** `slog.Info`, `slog.Debug`, `slog.Warn`, `slog.Error` and `slog.Log` are convenience wrappers that ask `slog.Default()` for a logger and call the corresponding method on it. `slog.SetDefault(l)` sets the logger that `slog.Default()` returns. That is the whole mechanism, and it is deliberately a single global: it exists so that a program can decide *once*, in `main`, where the output of every package-level call goes — including calls made from libraries you imported and do not control. ## What you get before you set anything A Go program that never calls `slog.SetDefault` still logs. `slog.Default()` returns a logger over a built-in handler that routes through the standard `log` package's default logger, which writes to standard error. The result is plain text with the `log` package's date/time prefix, then the level, then the message, then the attributes as `key=value`. It is readable, but it is not the structured output people expect from `slog`: it is not JSON, and it is not the `time=... level=... msg=...` form a `slog.TextHandler` produces. This matters more than it sounds. On an RPC service whose `main` wires up transport, config and a logger, everything logged **before** `slog.SetDefault` runs — package `init` functions, an imported package's startup message, a config-parsing warning — lands in that plain built-in format on stderr, while everything after it lands in your chosen format on your chosen writer. A newcomer reading `main` to find out where the logs go sees the JSON handler and reasonably assumes all records look like that; the first stray plain-text line in the collector is confusing precisely because nothing in `main` explains it. The fix is boring and effective: make `slog.SetDefault` the first meaningful statement in `main`, and keep logging out of package `init`. ## What SetDefault does not do - **It does not retro-fit existing loggers.** A `*slog.Logger` created earlier by `slog.New(otherHandler)` keeps its own handler forever. Handlers are attached at construction; `SetDefault` only moves the global pointer. - **It does not stack.** Calling it twice does not fan out to both handlers; the second call replaces the first. Fanning one record to several destinations needs a handler that does that on purpose. - **It is not a level switch.** Which records survive is the handler's business, not `SetDefault`'s. ## Who is allowed to call it By convention, exactly one place: the `main` package of a binary. `SetDefault` mutates process-wide state, so a library that calls it silently overrides a decision its importer already made, and the outcome depends on import and initialisation order — the worst kind of bug to reproduce. Libraries should accept a `*slog.Logger` (often as a field on a config struct, defaulting to `slog.Default()` when the caller leaves it nil) and log through that. ## The shape you will write over and over ```go func main() { h := slog.NewJSONHandler(os.Stdout, nil) slog.SetDefault(slog.New(h)) slog.Info("starting", "addr", ":8080") // ... build the server, which is handed its own logger explicitly } ``` The second argument to `slog.NewJSONHandler` is a `*slog.HandlerOptions`; passing `nil` means defaults. Both `os.Stdout` and `os.Stderr` are common choices for the writer — pick one deliberately, because a process that writes records to both streams is one a collector has to be told about twice. ## Interview framing The question is really "do you know that `slog.Info` is not the same thing as a logger you built, and do you know that the zero-configuration behaviour is *not* structured?" Say both, name `slog.New` / `slog.Default` / `slog.SetDefault` explicitly, and mention that ordering in `main` decides which records get the format you configured.
- Does slog.SetDefault change a *slog.Logger that some package already got from slog.New?No. A logger is bound to the handler it was constructed with, and nothing rebinds it later. `SetDefault` only changes what `slog.Default()` returns, which is what the package-level `slog.Info`/`slog.Error` functions use. If a package captured a logger at construction time, it keeps writing through that logger's handler regardless of how many times the default is replaced.
- What is the practical cost of calling slog.SetDefault late in main rather than first?Everything logged before it — package `init` functions, config parsing, an imported package's startup notice — goes to the built-in handler as plain text on stderr instead of your configured handler and writer. In a collector that only parses JSON on stdout those records are either malformed or missing entirely, and they are exactly the records you want when startup fails.
- Should a library call slog.SetDefault in its own init?No. It is process-global state owned by the binary's `main`. A library doing it silently overrides the importer's choice, and which one wins depends on initialisation order. The convention is that a library accepts a `*slog.Logger` from its caller and falls back to `slog.Default()` when none is supplied, so the importing program stays in control.
slog.New is buying a printer; slog.SetDefault is telling the whole office which printer is 'the' printer. Anything printed before that announcement comes out on the old machine in the corner.
saying these in an interview costs you the question
- Thinks slog.New alone changes what slog.Info writes
- Believes zero-config slog output is already JSON
- Assumes SetDefault rewires loggers created earlier
- Calls slog.SetDefault from a library's init function
- Cannot say where slog.Default writes before it is set