# Go - Hot-Path Micro-Optimizations A deferred unlock, a format call, or a variadic slice that crosses a linkname boundary costs nanoseconds per call. Those nanoseconds dominate when the path runs millions of times per second. Every rule on this page comes from a measured regression or a measured win in a Go tracing-library hot path. ## Key points - A getter that performs one map read under `mu.RLock()` plus `defer mu.RUnlock()` pays the defer and unlock cost on every call. When callers call the getter millions of times per second, that cost is non-trivial. The same getter returned the field directly with no lock and avoided the cost entirely. - Tag propagation that acquires a shared read lock on every inject call holds the lock for time that scales linearly with the tag count. A benchmark setup with 100 tags made the lock hold large enough to explain an entire W3C tag-injection regression. - An atomic store that replaces a direct field write on a handle shared by many goroutines adds measurable cache-line pressure. The pressure appears even when the field is not the bottleneck. - Direct `+` concatenation beats `strings.Join` on static pieces even when the compiler stack-allocates the slice literal. The compiler folds consecutive `+` operands into a single allocation. `Join` iterates per element and burns CPU on that iteration. One request-URL builder replaced `Join` with `+` and dropped geomean ns/op by 36.55 percent with zero change in alloc count. - `http.Header` is a map keyed by the canonical MIME form of the key. A case-insensitive `strings.EqualFold` scan over every key is replaceable with one direct map index. A lookup that computed `textproto.CanonicalMIMEHeaderKey` once at `init()` became about 15.6 times faster on the miss path, with zero allocations. - `fmt.Sprintf("%016x", ...)` costs about 47 ns and one allocation. A hand-written stack-array hex fill does the same work in about 7 ns with zero allocations. The win stayed invisible inside a 22-allocation W3C extraction benchmark until a focused micro-benchmark isolated the operation. `perfsprint` does not flag zero-padded hex because `strconv` has no equivalent, so linters do not catch this pattern. - A `//go:linkname` boundary forces a variadic options slice to the heap. A library replaced an exported function call with a bodyless `//go:linkname` to the same code, and the allocation count regressed. The boundary caused the regression, not the API change. Writing the options field by field into a config struct kept the slice away from the boundary and restored the exact allocation count. ## How to apply 1. Isolate the suspect operation in a focused micro-benchmark before you optimize. A nanosecond-scale win inside a 22-allocation benchmark stays invisible until you isolate the operation. 2. Remove `defer mu.RUnlock()` from getters that perform one map read. Return the field directly. 3. Acquire a shared read lock once per operation. Do not re-acquire per element when the element count scales. 4. Keep hot-path field writes direct. Add an atomic store only after a benchmark shows contention on the field. 5. Replace `strings.Join` on static pieces with direct `+` concatenation. 6. Compute `textproto.CanonicalMIMEHeaderKey` once at `init()` and index `http.Header` directly. Do not scan keys with `strings.EqualFold`. 7. Replace `fmt.Sprintf` zero-padded hex with a stack-array fill. Do not expect `perfsprint` to flag the pattern. 8. Keep variadic slices away from `//go:linkname` boundaries. Pass a config struct and write its fields directly. ## Observations - [concurrency] `defer mu.RUnlock()` in a getter that performs one map read adds non-trivial overhead at millions of calls per second. Return the field directly with no lock. #go (source: agent sessions, 2026-01) - [concurrency] A read lock acquired per inject call over propagating tags scales lock hold time linearly with the tag count. The lock hold explained an entire benchmark regression at 100 tags. #go (source: agent sessions, 2026-01) - [concurrency] An atomic store that replaces a direct field write on a handle shared by many goroutines adds cache-line pressure even when the field is not the bottleneck. #go (source: agent sessions, 2026-01) - [performance] Direct `+` beats `strings.Join` on static pieces. One URL builder dropped geomean ns/op by 36.55 percent with no alloc change. #go (source: agent sessions, 2026-07) - [performance] One `textproto.CanonicalMIMEHeaderKey` call at `init()` replaces a case-insensitive header-key scan. The miss path became about 15.6 times faster with zero allocations. #go (source: agent sessions, 2026-07) - [performance] A stack-array hex fill replaces `fmt.Sprintf("%016x")` at about 7 ns versus 47 ns, with zero allocations. #go (source: agent sessions, 2026-07) - [allocations] A `//go:linkname` boundary forces a variadic options slice to the heap. Field-by-field writes into a config struct restored the exact allocation count. #go (source: agent sessions, 2026-10) ## Relations - relates_to [[resources/documentation/Go/Benchmarks/Go Benchmarks - Statistics and benchstat]] (benchstat separates real micro-optimization wins from noise) - relates_to [[resources/documentation/Go/Go - Concurrency, Locks, and checklocks]] (lock and defer costs on hot getters) - relates_to [[resources/documentation/Go/Benchmarks/Go Benchmarks - Compiler Honesty and Dead Code Elimination]] (compiler allocation and folding behavior underpins the Join and linkname rules) ## Sources Personal agent-session notes, January 2026 to October 2026