Values

May 6, 2026

TIL Logger.LogAttrs is a thing! But what is that thing??

Yesterday I mentioned that using an slog.Attr can be marginally more efficient than using naked key/value pairs in a log call. While true, that glosses over what is likely to be a much more impactful performance consideration in certain applications…

Attrs and Values

…

The value part of an Attr is a type called Value. Like an [any], a Value can hold any Go value, but it can represent typical values, including all numbers and strings, without an allocation.

For the most efficient log output, use Logger.LogAttrs. It is similar to Logger.Log but accepts only Attrs, not alternating keys and values; this allows it, too, to avoid allocation.

The call

logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3))

is the most efficient way to achieve the same output as

slog.InfoContext(ctx, "hello", "count", 3)

So by using an slog.Attr and passing it to the LogAttrs method of the logger, rather than the much more convenient level-based methods (Info, Error, etc), you can avoid an extra memory allocation per attribute.

For many applications this won’t matter much. But there are applications where constructing and streaming logs adds a significant burden to the garbage collector. If this describes your application, consider using the less convenient LogAttrs—at least in hot paths.


Share this

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

Unsure? Browse the archive .

Related Content


Attribute equality

func (Attr) Equal func (a Attr) Equal(b Attr) bool Equal reports whether a and b have equal keys and values. That’s a pretty obvious, and opaque statement. How is equality determined? We have to look at the source to determine: func (a Attr) Equal(b Attr) bool { return a.Key == b.Key && a.Value.Equal(b.Value) } Okay, so it returns true if the keys are equal (that’s simple—they’re just strings) AND if the values are equal.


Expanding errors with ReplaceAttr

Let’s look at one other example where I reach for ReplaceAttr regularly: Logging additional error detail. (I talk more about this approach in my Boot.Dev course Learn Logging and Observability in Go (see below for a discount code).) For this technique to be meaningful, I first need errors that contain additional details. And this can take many forms, but probably the most ubiquitous form is an error that contains a stack trace, such as those produced by the popular package github.


Test logging with ReplaceAttr

Today’s topic is probably the most interesting, subtle, and complicated of the three HandlerOptions fields: ReplaceAttr. type HandlerOptions type HandlerOptions struct { … // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false.

Get daily content like this in your inbox!

Subscribe