When reading up on Go slices, I came across this behaviour in the context of the append
method
If the backing array of s is too small to fit all the given values a bigger array will be allocated. The returned slice will point to the newly allocated array.
Source - Golang Tour
To understand this I wrote the following piece of code:
func makeSlices() {
var a []int;
a = append(a, 0)
b := append(a, 1)
printSlice("b", b)
c := append(a, 2)
printSlice("b", b)
printSlice("c", c)
}
func printSlice(name string, s []int) {
fmt.Printf("var=%v len=%d cap=%d first_address=%v %v
", name, len(s), cap(s), &s[0], s)
}
Output:
var=b len=2 cap=2 first_address=0x414020 [0 1]
var=b len=2 cap=2 first_address=0x414020 [0 2]
var=c len=2 cap=2 first_address=0x414020 [0 2]
I would expect b
and c
to point to the same underlying array as they both are slices of the same length
But if I were to vary the same code for another length of slice:
func makeSlices() {
var a []int;
a = append(a, 0, 9)
d := append(a, 1, 2)
printSlice("d", d)
e := append(a, 3, 4)
printSlice("d", d)
printSlice("e", e)
}
Output:
var=d len=5 cap=8 first_address=0x450020 [0 0 9 1 2]
var=d len=5 cap=8 first_address=0x450020 [0 0 9 1 2]
var=e len=5 cap=8 first_address=0x450040 [0 0 9 3 4]
In this scenario, d
and e
should point to the same backing array as they are again slices of the same length but they do not.
Why this anomaly in behaviour? When exactly does Go decide to allocate a new backing array to a slice?
The answer is simple: append()
allocates a new backing array (and copies current content over) if the elements to be appended do not fit into the current capacity. Formally:
if len(s) + len(newElements) > cap(s) {
// Allocate new backing array
// copy content (s) over to new array
} else {
// Just resize existing slice
}
// append (copy) newElements
So for example if len=2, cap=4, you can append 2 elements, no allocation.
If len=2, cap=4, and you append 3 elements, then len+3 > cap, so a new backing array will be allocated (whose capacity will be greater than len+3, thinking of future growth, but its length will be 2+3=5).
In your first example you declare a slice variable that will have 0 length and capacity.
var a []int
fmt.Println(len(a), cap(a)) // Prints 0 0
When you do the first append, a new array will be allocated:
a = append(a, 0)
fmt.Println(len(a), cap(a)) // Prints 1 2
When you do another append, it fits into the capacity, so no allocation:
fmt.Println(len(a), cap(a)) // Prints 1 2
b := append(a, 1)
fmt.Println(len(b), cap(b)) // Prints 2 2
But this time you store the result slice in b
, not in a
. So if you do your 3rd append to a
, that still has length=1 and cap=2, so appending another element to a
won't require allocation:
fmt.Println(len(a), cap(a)) // Prints 1 2
c := append(a, 2)
fmt.Println(len(c), cap(c)) // Prints 2 2
So excluding the first append, all other append don't require allocation, hence the first allocated backing array is used for all a
, b
and c
slices, hence addresses of their first elements will be the same. This is what you see.
Again you create an empty slice (len=0, cap=0).
Then you do the first append: 2 elements:
a = append(a, 0, 9)
fmt.Println(len(a), cap(a)) // Prints 2 2
This allocates a new array with length = 2, so both length and capacity of the slice will be 2.
Then you do your 2nd append:
d := append(a, 1, 2)
fmt.Println(len(d), cap(d)) // Prints 4 4
Since there is no room for more elements, a new array is allocated. But you store the slice pointing to this new array in d
, not in a
. a
still points to the old array.
Then you do your 3rd append, but to a
(which points to the old array):
fmt.Println(len(a), cap(a)) // Prints 2 2
e := append(a, 3, 4)
fmt.Println(len(e), cap(e)) // Prints 4 4
Again, array of a
can't accommodate more elements, so a new array is allocated, which you store in e
.
So d
and e
have different backing arrays, and appending to any slice that shares a backing array with "another" slice doesn't (can't) change this "another" slice. So the result is that you see the same address for d
twice, and a different address for e
.
I added another few lines to your example, have a look here.
Look at the first printSlice("a", a)
. The length is 1, capacity is 2. When you add an item there is no need to allocated a larger underlying array, so the same array is used for b
and c
.
Once the length goes beyond 2, (d := append(c, 3)
), a new backing array is allocated for d
. c
remains unchanged. So when e
is created the same process occurs with another new backing array created.