Problem Statement
A Datadog engineer discovers their Go service is consuming 10x more memory than expected. The culprit: a slice memory leak. Explain slice internals and demonstrate how to avoid memory leaks.
Slice Header Structure
// Runtime representation (reflect.SliceHeader)
type SliceHeader struct {
Data uintptr // Pointer to underlying array
Len int // Number of elements
Cap int // Capacity of underlying array
}The Memory Leak Scenario
package main
import (
"fmt"
"runtime"
)
func processLargeData() []byte {
// Allocate 100MB
bigData := make([]byte, 100*1024*1024)
// Fill with data...
for i := range bigData {
bigData[i] = byte(i % 256)
}
// ❌ BUG: Return small slice of big array
// The entire 100MB stays in memory!
return bigData[:100]
}
func main() {
result := processLargeData()
runtime.GC()
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Result len: %d\n", len(result))
fmt.Printf("Heap in use: %d MB\n", m.HeapInuse/1024/1024)
// Output: Heap in use: 100 MB (leaked!)
}The Fix: Copy to New Slice
func processLargeDataFixed() []byte {
bigData := make([]byte, 100*1024*1024)
for i := range bigData {
bigData[i] = byte(i % 256)
}
// ✅ FIX: Copy to new slice
result := make([]byte, 100)
copy(result, bigData[:100])
return result
// bigData is now eligible for GC
}Slice Capacity Gotchas
func main() {
// len=5, cap=5
s := []int{1, 2, 3, 4, 5}
// Reslicing: len=3, cap=5 (shares underlying array!)
s2 := s[:3]
// Modify s2 affects s
s2[0] = 99
fmt.Println(s[0]) // 99
// Append within capacity: still shares array
s2 = append(s2, 10)
fmt.Println(s[3]) // 10 (!)
// Append beyond capacity: new array allocated
s2 = append(s2, 20, 30, 40)
s2[0] = 0
fmt.Println(s[0]) // 99 (s unaffected)
}Growth Strategy
func demonstrateGrowth() {
var s []int
prevCap := 0
for i := 0; i < 10000; i++ {
s = append(s, i)
if cap(s) != prevCap {
fmt.Printf("len=%5d cap=%5d growth=%.2fx\n",
len(s), cap(s), float64(cap(s))/float64(max(prevCap, 1)))
prevCap = cap(s)
}
}
}
// Output:
// len= 1 cap= 1 growth=1.00x
// len= 2 cap= 2 growth=2.00x
// len= 3 cap= 4 growth=2.00x
// len= 5 cap= 8 growth=2.00x
// ...
// len= 513 cap= 1024 growth=2.00x
// len= 1025 cap= 1280 growth=1.25x ← Growth slows after 1024Safe Slice Operations
// Force copy when returning subset
func safeSubset(data []byte, start, end int) []byte {
result := make([]byte, end-start)
copy(result, data[start:end])
return result
}
// Clear slice without reallocating
func clearSlice(s []int) []int {
return s[:0] // len=0, keeps capacity
}
// Full slice expression to limit capacity
func limitedSlice(s []int) []int {
// s[low:high:max] limits cap to max-low
return s[0:3:3] // cap is now 3, not len(s)
}Follow-up Questions
- What's the difference between a nil slice and an empty slice?
- Is it safe to concurrently append to the same slice?
- How do you efficiently delete an element from the middle of a slice?