Go 什么时候分配一个新的后备数组来切片?

Sin*_*ein 1 go slice

在阅读 Go 切片时,我在append方法的上下文中遇到了这种行为

如果 s 的后备数组太小而无法容纳所有给定的值,则会分配一个更大的数组。返回的切片将指向新分配的数组。
来源 - Golang Tour

为了理解这一点,我编写了以下代码:

去游乐场试试

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\n", name, len(s), cap(s), &s[0], s)
}
Run Code Online (Sandbox Code Playgroud)

输出:

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]
Run Code Online (Sandbox Code Playgroud)

我希望bc指向相同的底层数组,因为它们都是相同长度的切片


但是,如果我要为另一个切片长度更改相同的代码:

去游乐场试试

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)
}
Run Code Online (Sandbox Code Playgroud)

输出:

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]
Run Code Online (Sandbox Code Playgroud)

在这种情况下,dande应该指向相同的后备数组,因为它们又是相同长度的切片,但它们不是。


为什么会出现这种行为异常?Go 到底什么时候决定为切片分配一个新的后备数组?

icz*_*cza 5

答案很简单:append()如果要追加的元素不适合当前容量,则分配一个新的后备数组(并复制当前内容)。正式:

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
Run Code Online (Sandbox Code Playgroud)

例如,如果 len=2,cap=4,则可以追加 2 个元素,无需分配。

如果len=2,cap=4,你追加了3个元素,那么len+3>cap,那么就会分配一个新的backing array(容量会大于len+3,考虑到未来的增长,但是它的长度会为 2+3=5)。

解释你的第一个例子

在您的第一个示例中,您声明了一个长度和容量为 0 的切片变量。

var a []int
fmt.Println(len(a), cap(a)) // Prints 0 0
Run Code Online (Sandbox Code Playgroud)

当您进行第一次追加时,将分配一个新数组:

a = append(a, 0)
fmt.Println(len(a), cap(a)) // Prints 1 2
Run Code Online (Sandbox Code Playgroud)

当您执行另一个附加时,它适合容量,因此无需分配:

fmt.Println(len(a), cap(a)) // Prints 1 2
b := append(a, 1)
fmt.Println(len(b), cap(b)) // Prints 2 2
Run Code Online (Sandbox Code Playgroud)

但是这次您将结果切片存储在 中b,而不是 中a。因此,如果您将第 3 次追加到a,则仍然具有 length=1 和 cap=2,因此将另一个元素追加到a不需要分配:

fmt.Println(len(a), cap(a)) // Prints 1 2
c := append(a, 2)
fmt.Println(len(c), cap(c)) // Prints 2 2
Run Code Online (Sandbox Code Playgroud)

因此,除了第一个 append 之外,所有其他 append 都不需要分配,因此第一个分配的后备数组用于所有a,bcslice,因此它们的第一个元素的地址将相同。这就是你所看到的。

解释你的第二个例子

再次创建一个空切片(len=0,cap=0)。

然后你做第一个 append: 2 个元素:

a = append(a, 0, 9)
fmt.Println(len(a), cap(a)) // Prints 2 2
Run Code Online (Sandbox Code Playgroud)

这将分配一个长度为 2 的新数组,因此切片的长度和容量均为 2。

然后你做你的第二个附加:

d := append(a, 1, 2)
fmt.Println(len(d), cap(d)) // Prints 4 4
Run Code Online (Sandbox Code Playgroud)

由于没有空间容纳更多元素,因此分配了一个新数组。但是您将指向这个新数组的切片存储在 中d,而不是在 中aa仍然指向旧数组。

然后你做你的第三次追加,但是到a(指向旧数组):

fmt.Println(len(a), cap(a)) // Prints 2 2
e := append(a, 3, 4)
fmt.Println(len(e), cap(e)) // Prints 4 4
Run Code Online (Sandbox Code Playgroud)

同样,数组a不能容纳更多元素,因此分配了一个新数组,您将其存储在e.

因此,de具有不同的支持数组,并且附加到与“另一个”切片共享支持数组的任何切片不会(不能)更改此“另一个”切片。因此,结果是您看到d两次相同的地址,而e.