Concurrent logging

June 18, 2026

One last note in the performance section…

How does logging work in a concurrent system?

Performance considerations

…

The built-in handlers acquire a lock before calling io.Writer.Write to ensure that exactly one Record is written at a time in its entirety. Although each log record has a timestamp, the built-in handlers do not use that time to sort the written records. User-defined handlers are responsible for their own locking and sorting.

So three points fall out from this:

  1. The built-in logging handlers ensure it’s safe to log from multiple goroutines concurrently. You’ll never get a partial log from one interwoven with a log from another. This won’t happen:
2022/11/08 15:28:26 INFO hello 2022/11/08 15:28:27 INFO hello count=3count=3
  1. Timestamps are not used for sorting. So in theory, this could happen:
2022/11/08 15:28:28 INFO hello count=3
2022/11/08 15:28:27 INFO hello count=3
  1. If you’re implementing your own logger, these guarantees don’t apply, unless you implement them yourself.

Share this

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

Unsure? Browse the archive .

Related Content


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.


Handler level

Each handler has an optional Level value, which controls which logs to drop, and which to actually log: type HandlerOptions type HandlerOptions struct { … // Level reports the minimum record level that will be logged. // The handler discards records with lower levels. // If Level is nil, the handler assumes LevelInfo. // The handler calls Level.Level for each record processed; // to adjust the minimum level dynamically, use a LevelVar.

Get daily content like this in your inbox!

Subscribe