What is slice aliasing, and how can it cause subtle bugs?

5 minadvancedgoslicesaliasinggotchas

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.