When Go Hits Its Performance Ceiling: 9 Real Limitations Under Load
Go is a great programming language, and was created specifically for high-load service tasks. Its popularity is well deserved, and almost always it does its job well. Yet in environments with really intensive load, some features of Golang itself have to be taken into account as they directly affect performance.
1. Maps Don't Scale Perfectly
userIndex := make(map[string]*User)
With millions of records and long strings — cache misses and GC overhead.
Solution: Manually shard maps, or replace with []T + binary search
for
read-only indexes.
2. No String Interning
Response{Status: "success"}
Repeated strings are created anew with each parsing/formatting.
Solution: Use strings.Builder
for generation and manual string pool for
parsing.
3. interface{} and map[string]interface{} → Performance Killers
var data interface{}
json.Unmarshal(payload, &data)
Known as "interface pollution". Without typing, it leads to heap allocations and extensive usage of expensive "reflect".
Solution: Strictly typed struct
with json
tags.
4. Slice append = Hidden Copying
results = append(results, newResult)
When reaching cap
— full memcpy
, which hurts with 1M+ elements.
Solution: Pre-allocate buffer: make([]T, 0, estimate)
.
5. Many Goroutines → Load on Scheduler and GC
go handleConn(conn)
High churn creates overhead on scheduler and garbage collector.
Solution: Worker pool with fixed number of goroutines.
6. sync.Mutex = Contention Under High Competition
mu.Lock()
...
mu.Unlock()
On multi-threaded hardware and hot paths — latency breakdowns.
Solution: Break into smaller zones, use atomics or lock-free.
7. No Control Over Memory Layout
Structures scatter across heap — no alignment, no predictability.
Solution: This is Go's foundation. If critical — cgo
, or extract hot-path to
C/Rust.
8. Pointer Slices → Lost Cache Locality
users := []*User{&User{ID: 1}, &User{ID: 2}}
Pointers stored next to each other, but actual objects — scattered across heap. Iteration over
[]*User
= continuous cache misses on large volumes.
Solution: Use []User
if nil, mutability, or shared references aren't required.
9. json.Unmarshal vs json.NewDecoder
json.Unmarshal(data, &dst) // vs json.NewDecoder(reader).Decode(&dst)
Unmarshal
reads everything into memory → ok for small volumes.
Decoder
processes streaming → better for large volumes or streaming.
Solution: Unmarshal
for small JSON, Decoder
— for HTTP, files,
and large structures.
Conclusion
Go is a reliable working tool, but in some situations it doesn't provide the required level of control (by design), doesn't cache strings, and doesn't save from ineffective memory layouts. Understanding of its limitations is another step to mature systems' design.