How do slog.NewTextHandler and slog.NewJSONHandler differ in what they emit for the same record?
answer
- same record, two grammars
- readable pairs versus a parseable object
- same built-in keys either way
- the difference shows on a nested value
- one side falls back to fmt's %+v
basics
~10 sBoth write one line per record with the same built-in keys, but TextHandler emits key=value pairs for a human and JSONHandler emits an object for a machine. They disagree on nested values.
solid answer
~40 s`slog.NewTextHandler(w, opts)` writes one `key=value` line per record — `time=... level=INFO msg=... path=/v1/orders` — quoting any value that contains spaces or quotes. `slog.NewJSONHandler(w, opts)` writes one JSON object per record with the same built-in keys `time`, `level` and `msg`. Both take the same `*slog.HandlerOptions`, both render the time in RFC3339 with millisecond precision, and neither deduplicates attribute keys, so JSON output can legitimately contain the same key twice. The real difference shows up with caller-supplied values of unknown shape: the JSON handler encodes a map or struct through `encoding/json`, honouring `json.Marshaler` and struct tags, so nesting survives. The text handler uses `encoding.TextMarshaler` if the value implements it and otherwise renders it with `fmt`'s `%+v`, collapsing the whole value into one opaque string a parser cannot get back inside.
code
go · 7 linesmeta := map[string]any{"id": 7, "region": "eu"}
slog.New(slog.NewTextHandler(os.Stdout, nil)).Info("req", "meta", meta)
// text: the map is flattened by fmt's %+v into one opaque value
slog.New(slog.NewJSONHandler(os.Stdout, nil)).Info("req", "meta", meta)
// JSON: the map is encoded by encoding/json as a nested objectgo deeper
Know that both constructors take an io.Writer and options, that text gives you readable key=value pairs and JSON gives a machine-readable object, and that switching between them is one line in main.
Explain the mechanics: the same built-in time/level/msg keys, quoting in text output, and the value-rendering split where JSON uses encoding/json and text falls back to fmt's %+v for anything without a TextMarshaler.
Argue the operational consequence — a flattened nested value cannot be indexed or queried downstream, duplicate keys are ambiguous to consumers, and an encoding failure is not reported back to the call site because Handle's error is discarded.
Frame the format as a fleet-level decision with a migration cost: everything that parses the stream is coupled to it, so changing handler after the fact is a coordinated change, not a code cleanup.
## The same record, two grammars A `slog.Record` is a time, a level, a message and a list of attributes. A `slog.Handler` decides how that becomes bytes. The standard library ships two: - **`slog.NewTextHandler(w io.Writer, opts *slog.HandlerOptions) *slog.TextHandler`** — one line of space-separated `key=value` pairs. - **`slog.NewJSONHandler(w io.Writer, opts *slog.HandlerOptions) *slog.JSONHandler`** — one JSON object per line. Passing `nil` for the options is legal and means defaults. Both are safe for concurrent use by many goroutines, and both write each record as a single `Write` call to the underlying writer, which is what keeps records from interleaving mid-line. ### The built-in keys are the same Both emit `time`, `level` and `msg` for every record, in that order, before the attributes. (`source` appears too, but only when `HandlerOptions.AddSource` is set.) Both format the time as RFC3339 with millisecond precision. So a text line looks like ``` time=2026-09-01T10:00:00.000Z level=INFO msg=request path=/v1/orders ``` and the JSON equivalent is ```json {"time":"2026-09-01T10:00:00.000Z","level":"INFO","msg":"request","path":"/v1/orders"} ``` ### Where they diverge: values of unknown shape This is the part that decides which one you can actually operate on. Suppose a handler logs a value it received from a caller and does not control the shape of — a decoded `map[string]any`, a struct someone added a field to last week. - **JSONHandler** encodes the value with `encoding/json` semantics. A map becomes a nested JSON object, a struct becomes an object using its `json` struct tags, and a type implementing `json.Marshaler` gets to decide its own representation. The nesting is preserved, so a query engine downstream can index `meta.id` as a field. - **TextHandler** first checks whether the value implements `encoding.TextMarshaler` and uses that if so. Otherwise it falls back to `fmt`'s `%+v`. A map becomes something like `map[id:7]`; a struct becomes `{7 alice}` or, with `%+v`, `{ID:7 Name:alice}`. Structure is gone — you have one string, and any consumer wanting a field out of it is writing a regex. That asymmetry is the whole argument for JSON in anything a collector ingests, and the whole argument for text when the reader is a person staring at a terminal. ### Quoting The text handler has to keep its `key=value` grammar parseable, so it quotes values containing spaces, quotes or other characters that would break the form. A message like `msg="user not found"` is quoted; `path=/v1/orders` is not. The JSON handler has no such choice to make — `encoding/json` escapes everything for it. ### Duplicate keys Neither handler deduplicates. If a record ends up with two attributes named `id` — easy to do when a logger already carries one from a `With` call and the call site adds another — the text line has `id=` twice and the JSON object has the same key twice. That is valid JSON to produce and ambiguous to consume: most decoders keep the last one. Treat it as a lint problem in your own code rather than expecting the handler to fix it. ### Encoding failures `slog.Handler`'s `Handle` method returns an `error`, but the `Logger` convenience methods (`Info`, `Error`, ...) discard it. So if a value cannot be encoded — a type whose `MarshalJSON` fails, for instance — the failure is not going to surface as a returned error at the call site. If you are logging arbitrary caller-supplied data through the JSON handler, that is worth knowing before you rely on the record being there. ### Choosing The usual posture: JSON to `os.Stdout` in anything a collector reads, text when a human reads the stream directly — a CLI, a local run, a test. Because both handlers satisfy the same `slog.Handler` interface and take the same options, the choice is one line in `main` and nothing else in the program changes. What you should *not* do is emit both formats into the same stream: whatever parses it now has to guess per line. ### Interview framing Name both constructors, say the built-in keys are the same, then go straight to the value-rendering difference — that is the part that shows you have operated on the output rather than just configured it once.
- A struct logged through slog.NewJSONHandler comes out with different field names than expected. Why?Because the JSON handler encodes values with `encoding/json`, which honours `json` struct tags and skips unexported fields. Whatever tags the type carries for its API responses are also what the log record uses. If the type implements `json.Marshaler`, that method decides the output entirely — including producing something quite different from the field list.
- Can you emit both text and JSON without logging every record twice at the call site?Yes — put a handler in front that forwards one record to several handlers, one text and one JSON, each with its own writer. The call site stays a single `slog.Info`. What you should avoid is two handlers writing different formats into the *same* stream, because whatever parses that stream then has to detect the format per line.
- Do slog.NewTextHandler and slog.NewJSONHandler need different options?No. Both take the same `*slog.HandlerOptions`, so settings such as `AddSource` behave identically and switching handler is genuinely a one-line change in `main`. Passing `nil` uses defaults for both. That symmetry is deliberate: the format is meant to be an environment decision, not a code-structure decision.
saying these in an interview costs you the question
- Thinks TextHandler is what slog uses with no configuration
- Expects nested maps to keep structure in text output
- Believes duplicate attribute keys are deduplicated
- Assumes the two handlers take different option types
- Writes both formats into the same output stream