slog.Handler.WithAttrs

September 3, 2026

type Handler

type Handler interface {
  …

	// WithAttrs returns a new Handler whose attributes consist of
	// both the receiver's attributes and the arguments.
	// The Handler owns the slice: it may retain, modify or discard it.
	WithAttrs(attrs []Attr) Handler

This is likely the most interesting and subtle method of the slog.Handler interface. The important thing to note is that the handler is not mutated in place. Rather, a new handler is meant to be returned.

Why?

Two primary reasons I can think of:

  1. It makes it safe to re-use a logger without extra machinery. Recall that we can do things like this with a logger:
lg := logger.With("cart_id", cartID)
item, err := db.GetCart(cartID)
if err != nil {
  lg.Error("failed to get cart", "error", err)
} else {
  for _, item := cart.Items {
    go func() {
      lg = lg.With("item_id", item.ID)
      if err := processItem(item) {
        lg.Error("failed to process item", "err", error)
      }
    }()
  }
}

If the calling With on the logger mutated the logger, this above code would not only be racy, but it would accumulate unwanted fields, and could emit a log something like:

level=ERROR cart_id=123 item_id=89 item_id=96 item_id=13 error="item not found" msg="failed to process item"

Obviously not useful.

This could of course be handled in the logger itself, by cloning the handler for each With call, but it’s just much simpler to let each handler do that on its own. It’s also allows for much greater efficiency, because each handler can decide on its own internal representation, which is the second reason:

  1. It’s more efficient

It’s left up to each handler how to implement its own internal representation, but by leaving that up to the handler, rather than the logger handling cloning, the handler can make certain optimizations.

When using the standard library’s text and JSON handlers, for instance, WithAttrs formats the new attributes immediately, and stores their formatted representation, rather than storing them as slog.Attr fields. This is to avoid re-formatting for every actual log call.

To illustrate:

lg := logger.With("foo", "bar")
lg.Info("A")
lg.Info("B")
lg.Info("C")

produces:

level=INFO foo=bar msg=A
level=INFO foo=bar msg=B
level=INFO foo=bar msg=C

But the "foo=bar" formatting happened only once, and was cached, rather than formatted three times.

This does have one small possible downside:

lg := logger.With("foo", "bar")
if err := doSomething(); err != nil {
  lg.Error("failed", "error", err)
}

This code formats "foo=bar" once, even though it’s never emitted.


Share this

Direct to your inbox, daily. I respect your privacy .

Unsure? Browse the archive .

Related Content


slog.Handler.Handle()

I had not intended to take a month off, but then GopherCon came, and life happened… but now I’m back, and ready to rock and roll again… Today we’re to the core of the Handler interface: the Handle method. type Handler type Handler interface { … // Handle handles the Record. // It will only be called when Enabled returns true. // The Context argument is as for Enabled. // It is present solely to provide Handlers access to the context's values.


slog Handler methods

slog.Handler is an interface type. We’re going to look at each method on that interface in turn. Understanding these methods is valuable if you’re ever implementing your own handler. If you’re only using slog to produce logs using existing handlers, you could skip this part… though maybe you’ll learn something interesting anyway! type Handler type Handler interface { // Enabled reports whether the handler handles records at the given level. // The handler ignores records whose level is lower.


Anatomy of a log/slog logger

Unlike the older log package, which provides a single *log.Logger type as its primary interface, log/slog has a two-tiered architecture. This is roughly the same architecture used by the database/sql package: One interface implements a handler (or “driver” for database/sql), and another interface is consumed. The package itself provides the intermediate translation. This is essentially a localized example of ports-and-adaptors or hexagonal architecture. Here’s how the GoDoc for the package explains it:

Get daily content like this in your inbox!

Subscribe