【问题标题】:When does Golang append() create a new slice?Golang append() 什么时候创建一个新切片?
【发布时间】:2015-03-22 19:52:43
【问题描述】:

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

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

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) 不够大)。根据this answer,发送给AddOption的切片描述符应该是一个副本;这不是真的吗?据我所知,作为 AddOption() 的第二个参数发送的值要么是指向切片描述符的指针,要么 append() 没有返回新切片。

【问题讨论】:

标签: go slice


【解决方案1】:

参考: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 的修改不再导致对基础的修改 a 指向的数组。一个合乎逻辑的期望是:当 b 达到极限,a 和 b 都指向同一个新分配的 底层数组,而不是继续指向旧数组。

有多个切片指向同一个底层数组,频繁的append 操作可能会变得很棘手。更多信息请参见上面的链接。

【讨论】:

    【解决方案2】:

    您将切片、数据类型与实际表示混淆了。 The slice descriptor 由一对 int 组成,一个用于 len,一个用于 cap,以及一个指向底层数据的指针。

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

    编辑:这是一个代码 sn-p 来说明我的观点:

    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 共享相同的数据ptr。如果将容量更改为 4,则会打印:

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

    【讨论】:

    • 你是说 append() '返回一个新切片'实际上只是简单地制作一个相同的切片描述符,但增加容量值?我很欣赏切片表示方式的细微差别,但我对“新切片”的理解是分配一个带有数据副本的全新(扩展)内存块。似乎问题在于,根据 FUZxxl 的回答,容量扩展了多个元素,因此不会在每次迭代时创建新切片。
    • @lambda 我不是。 FUZxxl 是正确的,因为容量超过了可用容量,因此数据 ptr 是共享的。我添加了一个代码 sn-p 来说明我的观点,即两个切片都使用相同的数据 ptr。
    • 太好了,感谢您的澄清。我的关键错误是假设 append() 只将切片扩展了一个元素。
    【解决方案3】:

    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

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

    【讨论】:

      猜你喜欢
      • 2011-01-11
      • 1970-01-01
      • 2014-02-07
      • 2019-07-29
      • 2012-11-13
      • 2019-04-21
      • 2016-05-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多