An unexpected effect with Go Slices

Tom Elliott
average-coder
Published in
2 min readApr 4, 2019
A different kind of slice. Specifically, quiche.

Go’s slices can be pretty powerful, providing an array-like construct that can be dynamically re-sized at will, and grown in a reasonably memory efficient manner via append. But this comes with a few little surprises.

For example, there’s this little snippet:

s := []int{1, 2, 3}
s2 := append(s[:2], 4)
fmt.Println(s)
fmt.Println(s2)

You might expect this to output:

[1 2 3]
[1 2 4]

But what you actually get is:

[1 2 4]
[1 2 4]

Feel free to play around with this in the Go Playground.

What happens here is that the sub-slice we take with s[:2] stays in the same place in memory as the original slice. They’re actually just two separate references to the same underlying array.

When we then append to this sub-slice, the append operation has no idea that the original slice extends slightly beyond the second. The sub slice has a size of 2, but a capacity of 3 (the underlying array), so append can add a single element without having to grow the array-which would require copying everything to somewhere else in memory.

If we modify the append to add two elements:

s2 := append(s[:2], 4, 5)

We no longer have enough additional capacity and need to create a whole new copy of the array with more headroom. So making the above change results in the two slices pointing to distinct arrays and leaves s untouched.

[1 2 3]
[1 2 4 5]

This is a small gotcha, but can easily cause a lot of confusion when working with complex objects. I came across this in the wild when rendering a soy template, which added a whole other layer of confusion. It certainly tested my assumptions!

There’s a lot more about the ins and outs of Go’s slice implementation on the Go Blog.

--

--