YURKOL Ltd - Custom Software and Cloud Architectural Solutions for Modern Businesses

Hidden Memory Growth in Go Slices

Some hidden mechanic in the 'append' function in Golang can lead to seemingly unexpected behavior. A slice may grow from 1MB to 32MB, and this can be missed by a programmer.

Growth Pattern

Let's look at a simple example that demonstrates this behavior:

primes := make([]int, 0, 1024*1024) // capacity = 1MB

for i := 0; i < 2*1024*1024; i++ {
    primes = append(primes, i)
    // Internal growth logic varies:
    // < 1024: doubles capacity
    // > 1024: complex growth pattern
}
            

The internal growth strategy in Go's runtime changes based on slice size. For small slices (less than 1024 elements), capacity doubles on each growth. For larger slices, the growth follows a more complex pattern balancing memory usage and performance.

Memory Analysis

Such memory allocation patterns can be easily observed using pprof:

import "runtime/pprof"

f, _ := os.Create("mem.prof")
pprof.WriteHeapProfile(f)
f.Close()
                

After running this code, use go tool pprof mem.prof to analyze the allocation patterns.

Small Buffer Optimization

For slices with fewer than 256 elements, small buffer optimization can keep data on stack:

type Buffer struct {
    buf    [256]byte    // small buffer on stack
    slice  []byte       // points to buf or heap
}
                

This approach avoids heap allocations for small slices, improving performance for common cases.