Most Go performance advice still revolves around one metric:
reduce allocs/op.
Avoid heap allocations.
Preallocate everything.
Use sync.Pool.
Be careful with interfaces.
All of that sounds reasonable — and used to be useful.
But after benchmarking modern Go (1.25) in isolation, I realized that allocation count alone no longer predicts performance in a meaningful way.
One benchmark in particular completely changed my mental model.
Retention vs Allocation
Consider the following experiment.
Both variants below perform the exact same number of allocations.
The only difference is how much memory they retain.
Bad retention
- ~1.5 ms/op
- ~8 MB/op
- 129 allocs/op
Good retention
- ~90 µs/op
- ~11 KB/op
- 129 allocs/op
That’s a ~16× difference in runtime with identical allocation counts.
The GC doesn’t care how many objects you allocated.
It cares how much memory stays reachable — and for how long.
Once you see this, a lot of familiar advice starts to feel incomplete.
When allocs/op lies
I also benchmarked a few other “classic” optimization targets:
-
new(int)vs stack allocation → 0 allocs/op in both cases - interface vs concrete calls → measurable overhead, no allocation or GC cost
-
sync.Pool→ often pure overhead when allocations don’t hit the heap - slice over-preallocation → fewer allocations, worse performance
In isolation, many of these micro-optimizations either don’t matter anymore — or optimize the wrong layer.
The real shift in modern Go performance
What changed is not a single runtime tweak or compiler trick.
The shift is this:
Mechanical costs got cheaper.
Architectural costs dominate.
Modern Go rewards:
- clear ownership
- short-lived data
- explicit lifetimes
- bounded concurrency
Old advice assumed the runtime was fragile.
In modern Go, the runtime is usually fine — it’s accidental retention and unclear lifetimes that hurt.
The takeaway
Allocations still matter.
But allocation count alone is a poor proxy for performance.
If you reduce allocs/op without controlling object lifetime, you often optimize the wrong thing.
I published the full deep dive with all benchmarks, code, and detailed explanations here:
👉 https://blog.devflex.pro/why-most-go-performance-advice-is-outdated-go-125-edition
It covers:
- interfaces vs generics
-
sync.Pooltrade-offs - slice growth strategies
- retention vs allocation
- and why modern Go performance problems often look like architecture bugs
Top comments (1)
Curious what others consider their biggest Go performance blind spot today.