Go: Sentinel errors and errors.Is() slow your code down by 3000%
Performance of errors.Is in Go
- Benchmarks showed
errors.Issignificantly slower than a boolean check; later correction reduced the claim from “3000% / 30x” to roughly “500% / 5x” for the microbenchmark. - Several commenters stress the absolute times are tiny (single‑digit tens of nanoseconds) and usually dwarfed by I/O, but acknowledge it can matter in very hot in‑memory loops.
- Key costs:
- Pre‑fix: no fast path for
err == nil, so even the happy path paid reflection and tree‑walk overhead. - Implementation uses reflection and walks wrapped error chains.
- Inlining behavior matters; when functions were inlined, some benchmark variants were unrealistically optimized away.
- Pre‑fix: no fast path for
Go design & upcoming improvements
- Newer (unreleased) Go adds a fast check for
err == nil || target == nilinerrors.Is, expected to reduce overhead on the happy path. - Some discuss possible further optimizations (splitting reflection out to allow better inlining), and an ongoing inliner improvement effort.
Sentinel errors vs booleans (ok pattern)
- Debate over whether “not found” should be a sentinel error or a
boollike Go map lookups. - Arguments for
bool:- Faster in tight loops.
- “Not found” is often a normal outcome, not truly exceptional.
- Arguments for errors:
- Nice symmetry with other failure modes.
- Allows including which value/key was missing.
- Sentinel errors already used in stdlib (e.g.,
sql.ErrNoRows,io.EOF), though usually in slow operations.
Error construction vs inspection
- Multiple comments note most time is spent constructing errors (formatting strings, allocations, multiple wraps), not in
errors.Isitself. - Deeply wrapped errors (many
%wlayers) are especially expensive and often produce unreadable messages. - Some advocate wrapping on every layer for debuggability and structured logs, then only optimizing hot spots once profiling shows a problem.
- Others argue for treating errors as mostly opaque, avoiding pervasive wrapping and using stack traces and structured logging instead.
Broader language comparisons & philosophy
- Rust: discussion about exposing dependency error types,
#[non_exhaustive], and desire for exhaustive matching vs semver stability. - Java/C#/C++/JS: long side thread on “error vs exception” semantics and checked vs unchecked behavior.
- Some see Go’s error handling and
if err != nilpatterns (plus variable shadowing withok/err) as awkward; others consider it a reasonable trade‑off for simplicity and clarity. - Several emphasize profiling and avoiding premature optimization;
errors.Isis rarely the dominant cost in real systems.