What is slice aliasing, and how can it cause subtle bugs?
Quick Answer
Slice aliasing happens when two or more slices share the same underlying backing array, so writing through one is visible through the other — this occurs naturally whenever you slice an existing slice or array (s[1:3]), or when append doesn't need to reallocate. It causes bugs when code assumes a sub-slice is an independent copy, then modifies it and unexpectedly corrupts data the original slice (or another sub-slice of it) still relies on — a common real example is repeatedly slicing a shared buffer to parse chunks of data, then storing those sub-slices for later use, only to have a subsequent parse overwrite earlier "saved" results because they all alias the same array. The fix is an explicit copy() whenever a slice needs to outlive or be independent from the data it was sliced from.
Detailed Answer
Aliasing is a direct consequence of how slices work, but it produces bugs that look almost like magic if you don't expect them.
The setup
buf := make([]byte, 10)
a := buf[0:5]
b := buf[5:10]
a[0] = 'X'
fmt.Println(buf[0]) // 'X' — a and buf share the same backing array
This part is usually intuitive. The bug shows up when a slice is reused across iterations and results are saved without copying.
A realistic bug: reusing a buffer across a loop
func readChunks(r io.Reader) [][]byte {
buf := make([]byte, 4)
var chunks [][]byte
for {
n, err := r.Read(buf)
if n > 0 {
chunks = append(chunks, buf[:n]) // BUG: saves a slice aliasing the shared buf
}
if err != nil {
break
}
}
return chunks
}
Every entry in chunks is a sub-slice of the same buf. By the time the function returns, every entry in chunks reflects whatever the last Read call wrote into buf. None of them hold the data actually read at that point in the loop.
The fix: copy before saving
func readChunks(r io.Reader) [][]byte {
buf := make([]byte, 4)
var chunks [][]byte
for {
n, err := r.Read(buf)
if n > 0 {
chunk := make([]byte, n)
copy(chunk, buf[:n]) // independent copy, safe to keep
chunks = append(chunks, chunk)
}
if err != nil {
break
}
}
return chunks
}
The general rule
If a sub-slice needs to outlive, or be modified independently of, the slice or buffer it came from, copy it explicitly with copy(). Slicing alone never gives you independence — it only ever gives you a different view into the same data.