YURKOL Ltd - Custom Software and Cloud Architectural Solutions

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.