In Go, memory allocation can happen on either the stack or the heap. Stack allocations are far cheaper and reduce pressure on the garbage collector. The Go team has been working to move more allocations to the stack, especially for constant-sized slices, to improve program performance. Here we answer common questions about this optimization.
What makes stack allocations cheaper than heap allocations in Go?
Stack allocations are considerably cheaper because they involve simply moving the stack pointer—often zero-cost. The stack is automatically cleaned up when a function returns, so no garbage collector (GC) work is required. In contrast, heap allocations require a call to the memory allocator, which runs complex logic to find and reserve space. Heap allocations also generate work for the GC, which must later free that memory. Stack allocations are also more cache-friendly because they reuse recently accessed memory, improving locality. Even with recent GC advancements like the Green Tea collector, heap overhead remains significant.

How does dynamic slice growth cause multiple heap allocations?
When you use append in a loop, as in var tasks []task; for t := range c { tasks = append(tasks, t) }, the slice starts with no backing array. On the first iteration, it allocates a backing store of size 1. Once full, append allocates a new backing store of size 2, then 4, then 8, etc.—doubling each time. Each new allocation is a heap allocation. The old backing store becomes garbage, adding to GC load. While the doubling strategy eventually amortizes, the early iterations are wasteful: many small allocations occur before the slice grows large enough to fit subsequent items without reallocation.
What is the “startup phase” problem for slices?
In the early life of a dynamically growing slice, the backing store is small and fills up quickly, triggering frequent allocations. For example, the first four iterations allocate sizes 1, 2, 4, and then only the fourth iteration can append without a new allocation (because size 4 has space after three items). This startup phase is especially painful if the slice never becomes large—say, it only ever holds 3 or 5 items. Then you suffer multiple heap allocations for very little data, which is wasteful. This phase can dominate the cost in hot code paths where the slice is small and short-lived.
How can you avoid repeated heap allocations for slices that have a known maximum size?
If you know the maximum number of elements the slice will need, you can preallocate a backing array with make([]task, 0, maxSize). This single heap allocation (or stack allocation, if the compiler can prove the size) will be used for all appends, provided you don’t exceed the capacity. For constant-sized slices (size known at compile time), the Go compiler may even allocate the entire backing array on the stack, completely avoiding heap allocation. This is a key optimization the Go team has been improving: recognizing when a slice’s capacity is constant and small enough to live on the stack.
What recent Go changes help heap allocation?
In recent releases (around 2026), the Go compiler and runtime have been enhanced to detect more patterns where stack allocation is possible. For example, slices with a constant capacity determined at compile time can now be stack-allocated. Additionally, the runtime’s allocator has been tuned, but the main focus is on moving allocations out of the heap entirely. Changes like enabling the compiler to allocate small arrays (<2KB) on the stack, and improving escape analysis to reduce heap escapes, have reduced GC pressure and improved performance for typical Go programs.
Are there any trade-offs when using stack allocation for slices?
Stack allocation is limited by the stack’s size (usually small, e.g., 1KB per goroutine). If you allocate a slice with a large constant capacity on the stack, you risk stack overflow. Also, the slice backing array must not escape the function—if the slice is returned or stored in a heap-allocated structure, the compiler will put it on the heap. Thus, constant-sized stack allocation works best for small, temporary slices that are used locally and then discarded. For larger slices, preallocation with make on the heap may still be necessary, but you can avoid the repeated growth allocations.