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:
- 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:
- 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.