根据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是此代码的操场链接。在输出中:
[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在发送切片后立即返回,因此修改必须来自代码块
var newCombo []bool
for _, ch := range []bool{true, false} {
newCombo = append(combo, ch)
AddOption(c, newCombo, length-1)
}
但是,根据文档,append()应该返回一个新的切片(cap(combo)不够大)。根据{{3}},发送到AddOption的切片描述符应该是一个副本;那不是真的吗?据我所知,作为AddOption()的第二个参数发送的值要么是指向切片描述符的指针,要么append()不返回新的切片。
答案 0 :(得分:5)
当append()
创建新切片时,它不会创建仅比切片大一个切片的切片。它实际上创建的切片已经是比前一个更大的元素。看看这段代码:
package main
import "fmt"
func main() {
var sl []bool
for i := 0; i < 100; i++ {
sl = append(sl, true)
fmt.Println(cap(sl))
}
}
如果您运行此代码,您会发现容量最初会在每次分配时加倍;对于较大的切片尺寸,这种策略当然会改变。
答案 1 :(得分:4)
您将切片(数据类型)与实际表示混淆。 The slice descriptor由一对int组成,一个用于len,一个用于cap,以及指向底层数据的指针。
因此,append返回的确实是一个新的切片,传递给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
仍具有容量,因此a
和b
共享相同的数据ptr。如果将容量更改为4,则会打印:
[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 5]
答案 2 :(得分:0)
参考:http://criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/
根据链接:
Go采取更精益和懒惰的方法来做到这一点。它使 修改相同的底层数组,直到切片的容量为止 达到。
这与其他语言中切片的行为完全不同:
大多数语言(如Python)都会创建底层的另一个副本 当指向它的任何切片进行写操作时的数组。
提到的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]
在这里,在最后的验证步骤中,感觉有点怪异 对b的修改不再导致对底层的修改 数组指向的数组。合乎逻辑的期望是:b 达到限制,a和b都指向新分配的相同 底层数组,而不是继续指向较旧的数组。
多个切片指向同一个底层数组,频繁append
操作可能会变得棘手。在上面的链接中有更多相关内容。