Performance considerations

May 22, 2026

You’ve likely wondered why the log/slog package has some odd-looking functions and concepts in some places. Why do you set a handler’s level to a Leveler value, rather than a simple Level? Why so many ways to create key/value pairs ("key", "value" vs "key", slog.AnyValue("value") vs "key", slog.StringValue("value") vs slog.Any("key", "value") vs slog.String("key", "value"))?

It mostly comes down to one thing: Performance.

Or, more accurately, trying to balance performance with an easy-to-use API. These two goals are somewhat at odds. And that’s why we have more than one way to do many things in this package—the “easy” way, and the “performant” way.

Most often, you don’t need to care. Most parts of most apps are not performance critical.

But some parts of some apps ARE! And the next few days we’ll be coverting the doc section for those of us working on such problems.

Performance considerations

If profiling your application demonstrates that logging is taking significant time, the following suggestions may help.

If many log lines have a common attribute, use Logger.With to create a Logger with that attribute. The built-in handlers will format that attribute only once, at the call to Logger.With. The Handler interface is designed to allow that optimization, and a well-written Handler should take advantage of it.

This first hint is actually a happy place where the high-performance option overlaps with the easier-to-use case as well:

log.Info("starting request", "url", fullURL)
/* ... */
log.Info("request served", "url", fullURL)

Can be simplified:

log = log.With("url", fullURL)
log.Info("starting request")
/* ... */
log.Info("request served")

The benefit of log.With compounds the more keys you have, and the more times you need common log attributes.

I’ll typically use log.With when I construct my logger, to inject some common attributes into every log the application produces:

log := slog.New(...)
log = log.With(
  "app",        appName,
  "version",    appVersion,
  "git_sha",    gitSHA,
  "build_time", buildTime,
  "server_ip",  serverIP,
  /* etc, etc */
)

Share this

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

Unsure? Browse the archive .

Related Content


Logging common fields

It’s common that you’ll want to include certain attributes in all logs in an application or component. log/slog makes this pretty easy. Overview … Some attributes are common to many log calls. For example, you may wish to include the URL or trace identifier of a server request with all log events arising from the request. Rather than repeat the attribute with every log call, you can use Logger.With to construct a new Logger containing the attributes:


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