Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

Go ecosystem has many popular loggers and choosing one that you can use across all your projects is essential in order to keep a minimum of consistency. Ease of use and performance are usually the two metrics we consider in a logger. I will review them in Zap, the logger created by Uber.

Core ideas

Zap is based on three concepts that optimizes the performances, the first being:

Avoid interface{} in favor of strongly typed design.

This point leads to two other ones:

Reflection free. Reflection comes with a cost and could be avoided since the package is aware of the used types.

Free of allocation in the JSON encoding. If the standard library is well optimized, allocations here could easily be avoided, as the package holds all types of the parameters sent.

These points come with a small cost for the developer that forces them to declare each type when recording a message:

This explicit declaration of each field will allow the package to work efficiently during the logging process. Let’s review the design of the package to understand where those optimizations will happen.

Design

Before highlighting the optimized part of the package, let’s draw the global workflow of the logger:

Zap package workflow

One optimization we can see is the usage of the sync.Pool in order to avoid a systematic allocation while logging a message. Each message to be logged will re-use a structure created before and be released to the pool.

The second optimization concerns the encoder and the way the JSON is dumped. Each field to be logged is strongly typed as seen in the previous section. It allows the encoder to avoid reflection and allocation by directly dumping the value in a buffer:

optimized JSON encoder

This buffer is also managed thanks to the sync.Pool .

The trade-off performance/cost for the end-user is quite interesting since it does not require much effort from the developer to explicitly declare each field. However, the library provides a wrapper to the logger that exposes a more developer friendly API where you do not need to define each type of each field to be logged. Available from the method logger.Sugar() , it will slightly slow down and increase the number of allocations of the logger.

All those optimizations make the package quite fast and significantly reduce the allocations compared to the other package available in the Go ecosystem. Let’s make a tour and a comparison of the available alternatives.

Alternatives

The benchmarks provided by Zap clearly shows that Zerolog is the one that competes the most with Zap. Zerolog also provides benchmarks where the results are quite similar:

benchmarks from https://github.com/rs/zerolog

It clearly shows that Zerolog and Zap are far better in term of performance from the other packages, between 5 to 27 times faster.

Let’s now compare the same piece of code written with Zerolog:

It is quite close, and we can see that Zerolog also strongly types the parameter in order to optimize the performance. And as described the encoder interface, the JSON encoder also dumps data based on the type:

encoder interface in zerolog

Each entry sent to the logger, called event in Zerolog , also uses a pool from the sync package in order to avoid systematic allocation when logging a message.

As we can see, the packages are pretty similar. It explains why their performances are close to each other. Let’s try another package with a different design to understand the missing optimization we have seen in those packages.

Let’s now compare those logger with Logrus, another famous package in the Golang ecosystem. Here is the same code:

Internally, Logrus will also use a pool for the entry object but will add a layer of reflection when checking the fields sent along with the message. This reflection allows the logger to detect if all arguments passed to the logger are valid, but will slightly slow down the execution.

Also, contrary to Zap or Zerolog, the arguments are not typed and it will lead to having a layer of conversion of the original type to an empty interface and then back to the original type in order to encode it.

The package also adds an extra layer of lock on the hooks that could be removed if needed, but activate by default.

Optimizations for free

Reading the way those libraries are written is a good exercise for every Go developer in order to understand how to optimize our code and the potential benefits. Most of the time, for non-critical application, you will not need to go so deep, but if an external package like Zap or Zerolog comes with those optimizations for free, we should definitely take advantage of it.

If you would like to understand the potential benefits of using a pool, I suggest you read my article “Understand the design of sync.Pool”.