An unexpected effect with Go Slices
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.