【问题标题】:When does Go allocate a new backing array to slice?Go 什么时候分配一个新的后备数组来切片?
【发布时间】:2019-07-29 05:06:17
【问题描述】:

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

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

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

Try on the Go Playground

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)
}

输出:

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]

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


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

Try on the Go Playground

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)
}

输出:

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]

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


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

【问题讨论】:

    标签: go slice


    【解决方案1】:

    答案很简单:如果要追加的元素不适合当前容量,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
    

    所以例如如果 len=2, cap=4,你可以追加 2 个元素,没有分配。

    如果len=2,cap=4,你追加3个元素,那么len+3 > cap,所以会分配一个新的后备数组(其容量会大于len+3,考虑到未来的增长,但是它的长度将是 2+3=5)。

    解释你的第一个例子

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

    var a []int
    fmt.Println(len(a), cap(a)) // Prints 0 0
    

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

    a = append(a, 0)
    fmt.Println(len(a), cap(a)) // Prints 1 2
    

    当你做另一个追加时,它适合容量,所以没有分配:

    fmt.Println(len(a), cap(a)) // Prints 1 2
    b := append(a, 1)
    fmt.Println(len(b), cap(b)) // Prints 2 2
    

    但是这次你将结果切片存储在b,而不是a。因此,如果您对a 进行第三次附加,那么它仍然具有长度=1 和上限=2,因此将另一个元素附加到a 不需要分配:

    fmt.Println(len(a), cap(a)) // Prints 1 2
    c := append(a, 2)
    fmt.Println(len(c), cap(c)) // Prints 2 2
    

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

    解释你的第二个例子

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

    然后你做第一个追加:2个元素:

    a = append(a, 0, 9)
    fmt.Println(len(a), cap(a)) // Prints 2 2
    

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

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

    d := append(a, 1, 2)
    fmt.Println(len(d), cap(d)) // Prints 4 4
    

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

    然后您进行第三次附加,但指向a(指向旧数组):

    fmt.Println(len(a), cap(a)) // Prints 2 2
    e := append(a, 3, 4)
    fmt.Println(len(e), cap(e)) // Prints 4 4
    

    同样,a 的数组无法容纳更多元素,因此分配了一个新数组,将其存储在 e 中。

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

    【讨论】:

    • 我现在明白了。我应该查看原始切片 a 的属性,而不是派生切片的属性。当实际比较应该在 a-da-e 之间时,我对比较 de 感到困惑
    【解决方案2】:

    我在您的示例中添加了另外几行,看看here

    看第一个printSlice("a", a)。长度为1,容量为2。添加项目时无需分配更大的底层数组,因此bc使用相同的数组。

    一旦长度超过 2 (d := append(c, 3)),就会为 d 分配一个新的后备数组。 c 保持不变。因此,当创建 e 时,会发生相同的过程并创建另一个新的支持数组。

    【讨论】:

      猜你喜欢
      • 2016-06-24
      • 2015-03-22
      • 2022-12-18
      • 2012-08-13
      • 1970-01-01
      • 2022-01-25
      • 1970-01-01
      • 2012-11-10
      • 2019-12-16
      相关资源
      最近更新 更多