为什么Golang的切片操作如此复杂

It's my first day in Golang, and when I try its slice operation, like append(), there is one thing that makes me so confused:

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    a:= s[2:4];
    a = append(a, 1000, 1001);
    a[1] = 100; 
    printSlice("a:", a)
    printSlice("s:", s)
}

func printSlice(title string, s []int) {
    fmt.Printf("%s  len=%d cap=%d %v
", title,  len(s), cap(s), s)
}

When I append only two numbers to a, like:

a = append(a, 1000, 1001);

...the result is:

a:  len=4 cap=4 [5 100 1000 1001]
s:  len=6 cap=6 [2 3 5 100 1000 1001]

Which, I think, shows a as a reference to s.

But when I change that line of code to:

a = append(a, 1000, 1001, 1002);

...the result becomes:

a:  len=5 cap=8 [5 100 1000 1001 1002]
s:  len=6 cap=6 [2 3 5 7 11 13]

Which, I think, a has been reassigned to another segment of memory, to hold the whole thing, and detach the reference to s.

This is so inconsistent, and makes me so confused, that sometimes it is really easy to make this error (like when you have a random number of values to append).

Why is Golang designed like this? How can this be avoided, if I just want an operation like in JavaScript's slice and push?

This is a gotcha related to how slices are implemented in Go.

slice's struct looks like:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

So, a slice has a length and a capacity. If you try to append items to the slice such that it exceeds the current capacity then a new array is created underneath to hold the new data, but as the previous subslices may still be pointing to the older array it is kept as is until there are no more references left to it.


Now let's say we have a slice A: [1, 2, 3, 4, 5, 6] and a subslice B that points to last 3 items in A: [4, 5, 6].

[1, 2, 3, 4, 5, 6]
 ^        ^
 |        |
 |        B------
 |
 A---------------  

Now if we append an item to B then from your expected behaviour it should update A as well hence a new array will be created due to that. This can be inefficient if the size of subslice is small compared to the actual array(for appending 1 item copying 1000 items from original array).

Plus to keep it consistent all other references(subslices) that point to the old array will have to be updated to point to appropriate positions in the new array, that means we will have to store additional information in our slices, like start index. And this can get tricky if we have a subslice of a subslice.

Hence the current implementation makes sense.


The recommended approach here is to make a copy of subslice instead of working on it directly to prevent such issues. Another advantage of having a copy is that if the original slice is huge and has no references anymore then it can be garbage collected, but in case if it there was a subslice then the original array will be kept it memory till the subslice is still referencing to it.