TRT
Sign in
easyFoundationsdebugging

Explain Go's 24-byte slice header—when does `append` mutate an existing slice's backing array, how can slicing cause a 100 MB memory leak in Go, and how do.

Trace `(ptr, len, cap)`, `growslice` growth thresholds, backing-array retention leaks on small sub-slices, and three-index slicing `s[i:j:k]`.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

In Go, a slice []T is not a pointer to a dynamic object; it is a 24-byte value struct (runtime.slice) copied by value on assignment and function calls, containing three 8-byte fields: ptr (pointer to the first element in an underlying contiguous backing array), len (number of readable elements), and cap (total slots from ptr to the end of the backing array). Slicing b := a[0:2] on a 4-capacity array sets b.len = 2, b.cap = 4 sharing the same ptr! When you call append(b, x) and b.len < b.cap (2 < 4), append does not allocate a new array—it writes x directly into slot 2 of the shared backing array and returns a new 24-byte header with len = 3! To force append to allocate a fresh backing array instead of overwriting shared spare capacity, use a three-index slice expression a[low:high:max] (b := a[0:2:2]), which sets cap = max - low = 2 (len == cap).

Calling append(s, x) mutates the existing backing array at index s.len in-place if and only if s.len < s.cap; if s.len == s.cap (such as after capping capacity via s[i:j:j]), append allocates a new backing array and copies s.len elements without mutating the original backing array. Because a three-index slice b := a[i:j:j] sets b.len = j - i and b.cap = j - i, b has zero spare capacity (b.len == b.cap). Any subsequent append(b, ...) is forced to take the growslice allocation path, leaving a's backing array untouched.

Passing a 1,000,000-element []byte slice to a function copies only 24 bytes (ptr, len, cap) on the stack in ~1 nanosecond—never pass *[]T unless the callee needs to modify the caller's len or cap header without returning it! In production systems (Go runtime/slice.go (growslice), bytes.Clone() / slices.Clip() (which implements s[:len(s):len(s)]), and bufio.Scanner.Bytes() (which reuses the same backing array across tokens).), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how Go's 24-byte slice header (ptr, len, cap) and three-index capacity capping (s[i:j:k]) operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Passing []T to a Function, Three-Index Slice s[:n:n] / slices.Clip) with concrete numbers.
  • Identifies real-world failure modes (Storing a tiny slice id := bigPacket[4:20] in a long-lived map or struct) and explains how production systems mitigate them.

Common mistakes

  • Treating Go's 24-byte slice header (ptr, len, cap) and three-index capacity capping (s[i:j:k]) as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Branching two append calls off a shared prefix slice base that has len < cap).
  • Forgetting boundary conditions and error recovery (Appending 1,000,000 items to var s []T without pre-sizing when n is known).

This answer walks

Practice follow-ups

  1. 01Why does `bufio.Scanner.Bytes()` warn that the returned byte slice is only valid until the next `.Scan()` call?
  2. 02In Go 1.18+, how does `runtime.growslice` grow capacity once a slice exceeds 256 elements (`1.25x` smooth transition instead of pure `2x`)?

Not started

Sign in to save your learning progress.

Sign in to save