In Go, three names come up in every logging discussion: slog, zerolog and zap. "How do they differ and which one should I pick" is a standard interview question, and it deserves an answer not as a feature list but as an understanding of where each library came from and what price it pays.
Where the three libraries came from
Before Go 1.21 the standard library had no structured logging — only the primitive log with string concatenation. Two libraries filled that gap: zap from Uber (2016) and zerolog from the creator of fasthttp (2017). Both did the same thing — JSON logs with zero allocations on the hot path — and both became the de facto standard in high-load services.
Go 1.21 (August 2023) brought log/slog into the standard library. It absorbed the ideas of both: levels, key-value fields, JSONHandler, propagation through context.Context. The third-party libraries did not disappear — they stayed where their main advantage (speed) really matters, while everything new is written with slog, because it is in the standard library and its API has become the ecosystem's common language.
One statement — in three libraries
The differences show on the same action. First slog — it collects fields via slog.Any/slog.String and hands them to a handler:
// slog: standard library, works out of the box
logger.Info("order created", slog.String("order_id", id), slog.Int("total", 1250))
zerolog builds an event as a chain of calls and encodes it into a reusable buffer — its API repeats the fasthttp style:
// zerolog: chain of calls, zero allocations per event
log.Info().Str("order_id", id).Int("total", 1250).Msg("order created")
zap is configured explicitly and traditionally splits the fast path (typed API) from the convenient one (sugared):
// zap: sugared API for convenience, usually paired with NewProduction config
sugar.Infow("order created", "order_id", id, "total", 1250)
The same "order created" event is written differently, but you choose not by API — by whose price fits you.
How they differ in practice
Speed. In the classic benchmarks zerolog and zap are faster than slog: they encode an event into a reusable byte buffer with almost no allocations, while slog does extra work through interfaces per field. The difference only matters when a service writes thousands of log lines per request. For a typical service with a dozen lines per request it is within the noise — do not chase logger nanoseconds until pprof shows logging is the bottleneck.
Dependencies and integrations. slog has zero dependencies, every new library understands its Handler, and the otelslog bridge connects it to OpenTelemetry. zap and zerolog pull their API into every package that logs: switching from one library to another is a refactor of all call sites, not an import replacement.
Configuration. zap is traditionally the most flexible: sampling (write every hundredth debug line), different levels per package, an elaborate zap.Config. zerolog is simpler but covers the essentials: levels, fields, output to any writer.
A typical mistake is punting to fmt where fields are needed:
// don't: the string is built in the heap and the fields can't be searched in logs
log.Printf("order %s created, total=%d, user=%s", id, total, userID)
// do: an event with fields is encoded straight to JSON
logger.Info("order created", slog.String("order_id", id), slog.Int("total", total), slog.String("user_id", userID))
The second variant runs by itself — change the level and look at the JSON:
live example
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
logger.Info("order created", slog.String("order_id", "a-1024"), slog.Int("total", 1250))
}
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Three free days →
What to pick
A new service — slog. It is the standard, it is enough, the ecosystem moves its way, and an extra dependency is not needed. Very frequent logs on a hot path — zerolog: if pprof shows that logging eats a noticeable share of CPU, the chained zero-allocation API is the cheapest way out. An old service on zap or zerolog — leave it alone without a reason: working logging is not broken for fashion; migration is justified only when it really gets in the way (for example, unifying the observability platform).
Short version
- Before Go 1.21 the standard library had no structured logging — zap and zerolog provided it.
- slog — the standard since Go 1.21: zero dependencies, JSONHandler, the
otelslogbridge into OpenTelemetry. - zerolog — the
Info().Str().Msg()chain and a reusable buffer: minimal allocations per event. - zap — the most configurable: sampling and level configs; common in older services.
- Choice: new project — slog; hot path with thousands of log lines — zerolog; legacy — don't migrate without a reason.
What to read next
- Logging in Go: slog, JSON and context.Context — how slog works inside: fields, levels, requestId via context.
- Tracing in Go — how to connect logs to spans through OpenTelemetry.
- Metrics in Go — the second pillar of observability: what to count and how not to blow up the store with cardinality.