Golang 什么时候 append() 创建一个新的切片?

When does Golang append() create a new slice?

根据builtin api docs,当原切片的容量不够大时,append()将重新分配并复制到一个新的数组块。

这是一个(简化版)递归算法,用于创建字母表(在本例中为布尔值)的组合。字母表的成员 (true, false) 被递归地添加到切片中,直到它是正确的长度,此时它通过通道发送。

package main

import (
    "fmt"
)

func AddOption(c chan []bool, combo []bool, length int) {
    if length == 0 {
        fmt.Println(combo, "!")
        c <- combo
        return
    }
    var newCombo []bool
    for _, ch := range []bool{true, false} {
        newCombo = append(combo, ch)
        AddOption(c, newCombo, length-1)
    }
}

func main() {
    c := make(chan []bool)
    go func(c chan []bool) {
        defer close(c)
        AddOption(c, []bool{}, 4)
    }(c)
    for combination := range c {
        fmt.Println(combination)
    }
}

Here 是此代码的游乐场 link。在输出中:

[true true true true] !
[true true true false] !
[true true true false]
[true true true false]
[true true false true] !
[true true false false] !
[true true false false]
[true true false false]
[true false true true] !
[true false true false] !
[true false true false]
[true false true false]
[true false false true] !
[true false false false] !
[true false false false]
[true false false false]
[false true true true] !
[false true true false] !
[false true true false]
[false true true false]
[false true false true] !
[false true false false] !
[false true false false]
[false true false false]
[false false true true] !
[false false true false] !
[false false true false]
[false false true false]
[false false false true] !
[false false false false] !
[false false false false]
[false false false false]

以感叹号结尾的行是从 AddOption 发送到频道的行。那些没有的是出现在另一边的东西(即在 main() 中)。很明显,通过通道发送的切片在发送后发生了变化。

由于 AddOption returns 在发送切片后立即进行,修改必须来自代码块

var newCombo []bool
for _, ch := range []bool{true, false} {
    newCombo = append(combo, ch)
    AddOption(c, newCombo, length-1)
}

但是,根据文档,append() 应该 return 一个新的切片(cap(combo) 不够大)。根据this answer,发送给AddOption的切片描述符应该是一个副本;这不是真的吗?据我所知,作为 AddOption() 的第二个参数发送的值要么是指向切片描述符的指针,要么 append() 不是 returning 新切片。

append() 创建一个新的切片时,它不会创建一个只比之前的切片大一个的切片。它实际上创建了一个比前一个元素大几个元素的切片。看看这段代码:

package main

import "fmt"

func main() {
    var sl []bool

    for i := 0; i < 100; i++ {
        sl = append(sl, true)
        fmt.Println(cap(sl))
    }
}

Playground

如果您 运行 这段代码,您会看到容量最初在每次分配时都会翻倍;此策略当然会更改为更大的切片大小。

您将数据类型 slice 与实际表示混淆了。 The slice descriptor由一对int组成,一个是len,一个是cap,还有一个指向底层数据的指针。

所以,append returns 确实是一个新的切片,而传递给 add 选项的确实是切片描述符的副本。但是由于描述符有一个指向数据的指针,所以指针值(指向底层数据的地址)是相同的。

编辑:这是一个代码片段来说明我的观点:

package main

import "fmt"

func main() {
    s := make([]int, 0, 5)
    s = append(s, []int{1, 2, 3, 4}...)

    a := append(s, 5)
    fmt.Println(a)

    b := append(s, 6)
    fmt.Println(b)
    fmt.Println(a)
}

如果你run this,你会得到:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 6]

因为s还有容量,所以ab共享同一个数据指针。如果将容量更改为 4,它会打印:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 5]

参考:http://criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/

根据 link:

Go takes a more lean and lazy approach in doing this. It keeps modifying the same underlying array until the capacity of a slice is reached.

这与其他语言中切片的行为完全不同:

Most languages, like Python, create another copy of the underlying array when any of the slices pointing to it does a write.

提到的 example 的输出解释了行为。

Slice a len=7 cap=7 [0 0 0 0 0 0 0]
Slice b refers to the 2, 3, 4 indices in slice a. Hence, the capacity is 5 (= 7-2).
b := a[2:5]
Slice b len=3 cap=5 [0 0 0]

Modifying slice b, also modifies a, since they are pointing to the same underlying array.
b[0] = 9
Slice a len=7 cap=7 [0 0 9 0 0 0 0]
Slice b len=3 cap=5 [9 0 0]

Appending 1 to slice b. Overwrites a.
Slice a len=7 cap=7 [0 0 9 0 0 1 0]
Slice b len=4 cap=5 [9 0 0 1]

Appending 2 to slice b. Overwrites a.
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=5 cap=5 [9 0 0 1 2]

Appending 3 to slice b. Here, a new copy is made as the capacity is overloaded.
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 0 0 1 2 3]

Verifying slices a and b point to different underlying arrays after the capacity-overload in the previous step.
b[1] = 8
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 8 0 1 2 3]

Here, in the last verification-step, it feels a bit spooky that any modification to b is no more causing modification to the underlying array pointed to by a. A logical expectation would be that: when b hits the limit, a and b both point to the same newly allocated underlying array, instead of a continuing to point to the older one.

让多个切片指向同一个底层数组,频繁 append 操作会变得棘手。在上面的 link 中有更多相关信息。