skip to content

In Go's log/slog, what does Logger.InfoContext give you that Logger.Info does not?

level: juniorimportance: should knowfreq 55%

answer

  1. one extra parameter, different reach
  2. who else needs the context?
  3. Handle takes a ctx too
  4. the plain call substitutes an empty root

basics

~10 s

Logger.InfoContext passes your context.Context down to the handler's Handle method, so the handler can read request-scoped data such as a run id out of it. Logger.Info passes context.Background() instead, so that data never arrives.

solid answer

~50 s

They log the same message at the same level; the only difference is the context. `InfoContext(ctx, msg, args...)` hands `ctx` to the handler, whose method is `Handle(ctx context.Context, r slog.Record) error`, while `Info(msg, args...)` calls the handler with `context.Background()`. That matters as soon as anything in your logging pipeline reads the context: a handler that pulls a run id or request id out of `ctx` and stamps it on every record will find nothing on a plain `Info` call, so those lines come out uncorrelated and you cannot pull one run's output back together later. `slog` itself never inspects the context — it does not check cancellation or deadlines, it just carries it through to `Handler.Enabled` and `Handler.Handle`. The practical rule: if a `context.Context` is in scope, use the Context-taking method; the plain forms are for code that genuinely has none, such as startup.

code

go · 13 lines
go
type runIDKey struct{}

type runIDHandler struct{ slog.Handler }

func (h runIDHandler) Handle(ctx context.Context, r slog.Record) error {
	if id, ok := ctx.Value(runIDKey{}).(string); ok {
		r.AddAttrs(slog.String("run_id", id))
	}
	return h.Handler.Handle(ctx, r)
}

// logger.Info("batch done")             // handler gets context.Background(): no run_id
// logger.InfoContext(ctx, "batch done") // handler gets ctx: run_id is stamped

go deeper

for a junior

Be ready to name the pair precisely: Info takes a message and attributes, InfoContext takes a context.Context first. Say plainly that the context is the thing that reaches the handler.

for a middle

Explain that the plain methods call the handler with context.Background(), and that Handler.Handle's first parameter is where a context-aware handler reads request-scoped data such as a run id.

for a senior

Show the operational consequence: one Info call on a hot path produces a line with no correlation id, nothing fails, and nobody notices until an incident. Say how you keep call sites honest.

for a principal

Frame it as a house rule worth stating: standardise on the Context-taking methods wherever a context is in scope, accept the extra argument, and decide how review or tooling enforces it before the gap costs you an investigation.

## Two methods per level `log/slog`, in the standard library since Go 1.21, gives `*slog.Logger` two methods for each severity: - `func (l *Logger) Info(msg string, args ...any)` - `func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)` and the same pairing for `Debug`/`DebugContext`, `Warn`/`WarnContext`, `Error`/`ErrorContext`. The generic method `Logger.Log(ctx, level, msg, args...)` always takes a context. The package-level shorthands mirror this: `slog.Info(...)` and `slog.InfoContext(ctx, ...)` both log through the logger returned by `slog.Default()`. Both members of a pair build exactly the same `slog.Record` — same time, same level, same message, same attributes. Nothing about the formatted output differs because you chose one over the other. ## What the context is for A logger is a thin front end over a `slog.Handler`, which is an interface with four methods: ``` Enabled(context.Context, slog.Level) bool Handle(context.Context, slog.Record) error WithAttrs([]slog.Attr) slog.Handler WithGroup(string) slog.Handler ``` Two of those take a `context.Context`. That is the whole point of the Context-taking log methods: they are the only way a value living in a context reaches the code that formats the line. `InfoContext(ctx, ...)` forwards your `ctx` to `Enabled` and `Handle`; `Info(...)` forwards `context.Background()`, an empty root context that carries no values, no deadline and no cancellation. So a handler that does something like "if the context carries a run id, add `run_id` to the record" is silently a no-op for every `Info` call in the program. The line still appears, at the right level, with the right message — it just has no identity attached, which is precisely the line you will be missing at 3am when you are trying to reconstruct one customer's run out of a day of output. ## What the context is not for Three things people expect that do not happen: 1. **`slog` does not serialise the context.** Passing `ctx` does not dump its values into the output. Only what a handler deliberately extracts appears. 2. **`slog` does not honour cancellation.** A log call on an already-cancelled context still logs. `slog` never reads `ctx.Done()` or `ctx.Err()`; a handler is free to, but the built-in text and JSON handlers do not. 3. **The level is unaffected.** `InfoContext` is `LevelInfo`, exactly like `Info`. A handler may *use* the context to decide `Enabled` — for example raising verbosity for one flagged run — but that is a handler you wrote, not default behaviour. ## Which one to call The rule most teams settle on is mechanical: **if a `context.Context` is in scope, use the Context method.** Handler code, anything called from a handler, anything called from a worker that was handed a run's context — all of it uses `InfoContext`/`ErrorContext`. The plain forms remain useful where no context exists and none would mean anything: `main`, flag parsing, configuration loading, a fatal error before the first request. The cost of the rule is small (one extra argument) and the cost of breaking it is invisible at the time. Nothing fails, nothing errors, no test goes red — a field is simply absent from the output. That asymmetry is why the convention is worth stating out loud in a codebase rather than left to taste. ## Worth knowing Because the plain methods substitute `context.Background()` rather than refusing to run, code that logs through the package default (`slog.Info`) from a library you do not control also produces uncorrelated lines. If correlation matters, the lines you own must go through the Context path, and lines you do not own will not carry the id no matter what your handler does.

  • If Info drops the context, why does log/slog offer the non-context methods at all?
    Because plenty of code has no context to pass: `main`, configuration loading, package initialisation, a background daemon's startup path. Forcing every call site to invent a `context.Background()` argument would be noise. The pair exists so the short form is available where a context would be meaningless, not so you can skip the argument where one is in scope.
  • Does slog skip or shorten a log call when the context passed to InfoContext is already cancelled?
    No. `slog` never consults `ctx.Done()` or `ctx.Err()`; a cancelled context logs exactly like a live one. The context is carried through to `Handler.Enabled` and `Handler.Handle` and nothing else. A handler you write could choose to inspect cancellation, but the built-in text and JSON handlers do not.
  • Does the package-level slog.Info behave differently from a logger's Info method here?
    No — `slog.Info(msg, args...)` is shorthand for `slog.Default().Info(...)`, so it also hands the handler `context.Background()`. The context-carrying shorthand is `slog.InfoContext(ctx, msg, args...)`. Choosing the default logger changes which handler runs, not whether your context reaches it.

The context is the envelope the log call travels in. Info posts the message in a blank envelope, so whatever was written on the outside is gone by the time the handler opens it.

saying these in an interview costs you the question

  • Says InfoContext logs at a different level than Info
  • Thinks passing a context dumps its values into the output automatically
  • Believes slog skips the line when the context is cancelled
  • Assumes Info still sees the caller's context implicitly
  • Treats the two as interchangeable style choices