Boost Go Performance with slices.Grow: Pre‑allocate to Avoid Repeated Expansions
Using Go 1.21’s experimental slices.Grow function to pre‑allocate slice capacity can dramatically reduce allocation overhead and latency, as demonstrated by a real‑world log‑aggregation service where response time dropped from 80 ms to 25 ms and memory allocations fell by 70 %.
Slice growth overhead
When append exceeds a slice's capacity, Go allocates a larger array, copies the existing elements, and frees the old array. Each growth incurs a memory allocation and a copy, which can dominate CPU time in high‑frequency paths.
var data []int
for i := 0; i < 1000000; i++ {
data = append(data, i) // many reallocations
}In a log‑aggregation service memory allocation accounted for about 40 % of CPU time, leading to a panic runtime error: slice bounds out of range.
Go 1.21 experimental slices.Grow
slices.Growreserves capacity before appending.
import "slices"
s := []int{1, 2, 3}
s = slices.Grow(s, 5) // reserve space for 5 more elements
s = append(s, 4, 5, 6, 7, 8) // no extra allocationGrow only changes capacity, not length. After the call len(s) is unchanged while cap(s) may increase.
If the existing capacity is sufficient, Grow returns the original slice unchanged.
When capacity is insufficient, a new underlying array is allocated. The address of &s[0] changes, which can be observed with fmt.Printf("%p", &s[0]).
Real‑world case
Function aggregateEvents reads thousands of rows and appends each event to a slice.
func aggregateEvents(userID string) []Event {
var events []Event
rows := db.Query("SELECT ... WHERE user_id = ?", userID)
for rows.Next() {
var e Event
rows.Scan(&e)
events = append(events, e) // each append may trigger growth
}
return events
}When the number of events exceeded roughly 500, latency spiked and runtime.growslice dominated CPU usage in pprof.
Optimization with pre‑allocation
estimated := estimateEventCount(userID)
events := make([]Event, 0, 0)
events = slices.Grow(events, estimated) // pre‑reserve capacity
for rows.Next() {
var e Event
rows.Scan(&e)
events = append(events, e) // now almost zero extra allocations
}After applying the pre‑allocation, the 99th‑percentile latency dropped from ~80 ms to ~25 ms and memory allocations decreased by roughly 70 %.
Micro‑benchmark
Ordinary append : ~120 ms, ~20 allocations.
slices.Grow pre‑allocation : ~45 ms, 1 allocation.
Numbers are from a local benchmark; actual results depend on hardware and Go version.
Guidelines
Do not use when the final size is uncertain. Over‑estimating wastes memory.
Small slices (≈ < 100 elements) rarely benefit. The overhead of calling Grow can outweigh the cost of a few reallocations.
Frequent Grow followed by large slice release may fragment the heap. Consider pairing with sync.Pool in long‑running services.
When to apply
Only when an approximate element count greater than 100 is known and the code runs in a high‑frequency path.
Key takeaways
Understanding slice growth mechanics enables targeted performance improvements.
Pre‑allocating capacity with slices.Grow can dramatically reduce latency and allocation overhead.
The simplest change—calling a single library function—often yields the biggest gain.
Source: Huawei Cloud Community, golang学习记, https://bbs.huaweicloud.com/blogs/477729
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Golang Shines
We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
