Back to blog

Sunday, August 16, 2026

Why Did Modifying One Go Slice Change Another?

Title card for 'Why Did Modifying One Go Slice Change Another?' with a short code example

Because they share an underlying array. A slice is a pointer, a length, and a capacity, not a private copy of the elements. b := a[1:4] makes a new header that still points at a's array. b[0] = 99 writes that array. a sees it. There is no compiler warning.

The failure

This looks like two independent lists. It is not.

package main

import "fmt"

func main() {
    a := []int{10, 20, 30, 40, 50}
    b := a[1:4] // [20 30 40]

    b[0] = 99

    fmt.Println(b) // [99 30 40]
    fmt.Println(a) // [10 99 30 40 50]: a[1] changed
}

The same thing happens when you pass a slice into a function. Go copies the header. The copy still names the same array, so in-place writes leak back to the caller:

func zeroFirst(s []int) {
    s[0] = 0
}

func main() {
    a := []int{10, 20, 30}
    zeroFirst(a)
    fmt.Println(a) // [0 20 30]
}

That is not "Go sometimes passes by reference." The header was copied. The array was not.

Why this happens

The header is three machine words:

Slices a and b point at the same backing array; b[0] and a[1] are the same element, now 99

a:  ptr → [10] [20] [30] [40] [50]     len=5  cap=5
b:  ptr →      [20] [30] [40]          len=3  cap=4
                    ↑
                 same cells

a[1:4] does not allocate. It shifts the pointer and shortens len / cap. Assignment c := a is the same story: two headers, one array.

append makes this worse when there is spare capacity. append writes into the unused slots of the existing array. Those slots may already belong to another slice:

original := []int{1, 2, 3, 4, 5}
head := original[:2] // [1 2], cap 5

head = append(head, 99)
fmt.Println(head)     // [1 2 99]
fmt.Println(original) // [1 2 99 4 5]: original[2] was overwritten

If append has to grow past capacity, it allocates a new array and the sharing breaks. That is why the bug is intermittent: small appends corrupt the neighbor; a large append suddenly looks "safe."

The fix

If the two slices must not share memory, copy. copy writes element values into a destination you already sized. It never grows the destination.

a := []int{10, 20, 30, 40, 50}
b := make([]int, 3)
copy(b, a[1:4]) // b is [20 30 40] on its own array

b[0] = 99
fmt.Println(b) // [99 30 40]
fmt.Println(a) // [10 20 30 40 50]: unchanged

If you only need to stop a later append from writing into a neighbor, cap the capacity with a full slice expression s[low:high:max]:

original := []int{1, 2, 3, 4, 5}
head := original[:2:2] // len 2, cap 2: no spare room in original
head = append(head, 99)
fmt.Println(head)     // [1 2 99]
fmt.Println(original) // [1 2 3 4 5]: append allocated a new array

Keep sharing when you mean it (in-place reverse, sub-windows of a buffer you own). Copy when the slice leaves your function, sits in a struct, or is a small window into a large buffer you want the GC to free.