Walks back the v2.0.7 API break on the global helpers. v2.0.6 untyped call sites compile and run again with no source changes. v2.0.7 typed call sites continue to work too. The zero-alloc typed path moves to new *F entrypoints that opt in by name.
What changed
zlog.Debug / Info / Warn / Error / Fatal once again accept ...any and route correctly:
zlog.Info("user logged in", "username", "alice", "user_id", 12345) // 0 allocs (KV path)
zlog.Info("user logged in", zlog.String("username", "alice")) // 3 allocs (Field via ...any)zlog.DebugF / InfoF / WarnF / ErrorF / FatalF are new typed-only entrypoints that take ...Field directly:
zlog.InfoF("user logged in", zlog.String("username", "alice"), zlog.Int("user_id", 12345)) // 0 allocszlog.InfoKV / DebugKV / WarnKV / ErrorKV / FatalKV are kept as explicit aliases for the untyped path. Anyone who already migrated to them in v2.0.7 keeps working.
Why the rollback
v2.0.7's switch to Info(msg, fields ...Field) was strictly a perf decision — it shaved 3 allocs from the typed-via-global path by avoiding Field-into-...any boxing. But it was the wrong call for a non-major release: every existing v2.0.6 caller of the untyped style (zlog.Info("msg", "k", v)) became a compile error.
The structurally-correct design is two names — one for each variadic shape — added additively. v2.0.8 does that:
| Style | API | Allocations |
|---|---|---|
| Untyped key/value pairs | zlog.Info("msg", "k", v) |
0 |
| Typed Fields (compat) | zlog.Info("msg", String(...)) |
3 (Field→any boxing at callsite) |
| Typed Fields (zero-alloc) | zlog.InfoF("msg", String(...)) |
0 |
The 3-alloc cost on Info(...any) with typed Fields is structural: passing a 56-byte Field through a ...any parameter heap-boxes each argument at the Go callsite, before the function runs. No compiler trick on the callee side eliminates that. The InfoF entrypoint sidesteps it by taking ...Field directly.
Compatibility matrix
| Source style | v2.0.6 | v2.0.7 | v2.0.8 |
|---|---|---|---|
zlog.Info("msg", "k", v) (untyped) |
0 allocs | compile error | 0 allocs |
zlog.Info("msg", String(...)) (typed) |
silent drop bug | 0 allocs | 3 allocs |
zlog.InfoF("msg", String(...)) |
n/a | n/a | 0 allocs |
zlog.InfoKV("msg", "k", v) |
n/a | 0 allocs | 0 allocs |
Default().Info("msg", String(...)) |
0 allocs | 0 allocs | 0 allocs |
Migration
- Coming from v2.0.6: bump the dependency, no code changes. (You also pick up every v2.0.7 fix: the silent-drop bug, the
Bytes(empty)panic, theLogger.log65535 truncation, the proper MPMC ring buffer.) - Coming from v2.0.7: bump the dependency, no code changes. Typed
zlog.Info(...)calls now cost 3 allocs each — rename tozlog.InfoF(...)to keep zero-alloc behavior. The compiler doesn't catch this; it's a perf migration, not a correctness one.
Bench (Apple M5)
zlog.Info("msg", "k", v) 32 ns/op 0 allocs (KV)
zlog.InfoF("msg", String(...)) 34 ns/op 0 allocs (typed, no boxing)
zlog.Info("msg", String(...)) 87 ns/op 3 allocs (typed via ...any)
Default().Info("msg", String(...)) 34 ns/op 0 allocs (typed, instance)
Other changes
Carries forward all v2.0.7 fixes:
Bytes(nil)andBytes([]byte{})no longer panic.Logger.logclamps long messages to 65535 to match the uint16 header.Logger.FatalandStructuredLogger.Fatalalways exit, even when filtered.LogfmtWriter.Writeis zero-alloc on long records (one-pass size estimate).LogfmtWritercaches the formatted RFC3339 timestamp by Unix second.- AsyncWriter uses an LMAX Disruptor MPMC ring buffer (per-slot sequence numbers) — 212 ns/op end-to-end, race-clean.
Tests
Coverage for all three global-helper paths (KV via Info, typed via Info, typed via InfoF) plus the Fatal-when-filtered subprocess test. Full race-detector run is green; long-record alloc test gated //go:build !race because the race detector triggers GC frequently enough to clear sync.Pool's localcache for the 1 MiB-class buffers (a -race property, not a production bug).