languages/go-slices-why-appending-to-a-copy-can-change-the-original.md

Go slices: why appending to a copy can change the original

Programming languages · 4 min read ·

Change a copy of a Go slice and sometimes the original changes too. The reason is that a slice is only a small window onto an array, with a start, a length and a capacity.

Step through `append` yourself: with room to spare it writes into the same array, without room it makes a new one. Real Go 1.24 output shows both cases, plus how to copy safely and cap a window so it cannot overwrite its neighbour.

Five boxes in a row showing a Go slice a whose first three boxes are shared with slice b after an append

The surprise

You copy a list, change the copy, and the original changes too. Or you do the same thing and the original does not change. In Go, both can happen with the very same line of code, and the difference is a number you may not have looked at yet.

Go's flexible list type is called a slice. It is the thing you reach for in almost every Go program, so it is worth understanding what one really is.

A slice is a small window onto an array

Go has fixed-size arrays: a row of boxes that can never grow. A slice does not hold values itself. It is a tiny note with three facts: where in an array it starts, its length (len, how many boxes it shows) and its capacity (cap, how many boxes are available from its start to the end of the array).

Two slices can point at the same array. Then they are two windows onto the same boxes, and writing through one is visible through the other.

What append does

append(a, 4) adds a value after the last visible element. There are exactly two cases:

1. There is room (len < cap)

Go writes the value into the next spare box of the same array and returns a slice with len one higher. Nothing is copied. The old and new slices now share boxes.

2. There is no room (len == cap)

Go allocates a bigger array, copies the old values over, adds the new one, and returns a slice pointing at the new array. From then on the two slices are independent.

Try it

Pick the capacity, then run the lines in order and watch which boxes are shared.

The same thing in real code

Here is the "room" case as a complete program. We ran it with Go 1.24:

a := make([]int, 3, 4)   // len 3, cap 4
a[0], a[1], a[2] = 1, 2, 3
b := append(a, 4)        // room left: same array
b[0] = 99
fmt.Println(a, b)        // [99 2 3] [99 2 3 4]

c := append(b, 5)        // b is full: new array
c[0] = -1
fmt.Println(b, c)        // [99 2 3 4] [-1 2 3 4 5]

Notice that a changed when we wrote to b, but b did not change when we wrote to c. The first append had room; the second did not.

A slice written as a literal has no spare room at all:

x := []int{1, 2, 3}      // len 3, cap 3
y := append(x, 4)        // no room: new array
y[0] = 99
fmt.Println(x, y)        // [1 2 3] [99 2 3 4]

Here y got a fresh array, so x is untouched. With Go 1.24, cap(y) printed 6, not 4: when Go grows an array it often takes more than it needs, and the exact amount is up to the runtime, so never rely on a specific number.

Why it grows the way it does

If every append allocated an array exactly one box bigger, appending 1,000 items would copy values over and over. So Go over-allocates. Appending 20 numbers one at a time to an empty slice made the capacity go through 1 2 4 8 16 32 in our run: it roughly doubles for small slices. Most appends are then cheap, and the occasional copy is spread across many of them.

What this means for your code

Always use the result of append. Write a = append(a, v). The returned slice may point at a new array, and the old one may or may not have changed.

Do not assume a slice you were handed is yours. If a function appends to a slice parameter, it might write into spare boxes that belong to the caller's array. When you need an independent copy, make one on purpose:

copyOfA := make([]int, len(a))
copy(copyOfA, a)

You can cap the window. The form a[:2:2] gives a slice with len 2 and cap 2, so appending to it must allocate a new array instead of overwriting a's third box. In our run, d := a[:2:2] then append(d, 50) left a as [99 7 3].

The reverse trap is real too: append(a[:1], 7) wrote 7 into a's second box, because a[:1] still had spare capacity. That is the same rule as before, just in a less obvious place.

Remember

A slice is a window (start, length, capacity) onto an array. append reuses the array when there is room and makes a new one when there is not. If two slices might share an array, check cap before you trust that they are separate.

$ ls languages/

see all →
languages/generators-how-a-python-function-can-pause-in-the-middle-and-pick-up-w.md Generators: how a Python function can pause in the middle and pick up where it left off A generator is a Python function that can pause halfway, hand you a value, and carry on later from the same line with its variables intact… 4 min read · 25 Oct languages/javascript-s-event-loop-why-settimeout-0-runs-after-the-code-below-it.md JavaScript's event loop: why setTimeout(…, 0) runs after the code below it Why does `setTimeout(fn, 0)` run after the lines below it, and why does a promise callback beat it? JavaScript has one worker and two… 5 min read · 22 Oct languages/rust-ownership-why-let-b-a-makes-a-unusable.md Rust ownership: why let b = a makes a unusable Why does Rust refuse to compile `let b = a;` followed by a use of `a`? Because of ownership: every value has exactly one owner, and… 5 min read · 19 Oct languages/ruby-strings-why-changing-b-also-changes-a.md Ruby strings: why changing b also changes a In Ruby, `b = a` does not copy a string: it puts a second label on the same object. So `b << "s"` changes `a` too, while `b += "s"` does… 4 min read · 16 Oct languages/python-default-arguments-why-a-function-remembers-the-last-call.md Python default arguments: why a function remembers the last call Call a small Python function twice and the second call remembers the first. The cause is a default value like `basket=[]`, which Python… 4 min read · 13 Oct languages/why-does-5-3-give-53-in-javascript-type-coercion-step-by-step.md Why does "5" + 3 give "53" in JavaScript? Type coercion, step by step Why does `"5" + 3` give `"53"` while `"5" - 3` gives `2`? It is not random. JavaScript follows a short recipe called type coercion: it… 4 min read · 7 Oct