【问题标题】:unexpected slice append behaviour意外的切片追加行为
【发布时间】:2017-10-24 18:48:35
【问题描述】:

我今天在 go 代码中遇到了奇怪的行为:当我在循环中将 elements 附加到 slice 然后尝试根据循环结果创建新的 slices 时,最后一个 append 覆盖 slices 来自以前的appends

在这个特定示例中,这意味着 sliceFromLoop j,gh 切片的最后一个元素分别不是 100,101102,但...始终是 102

第二个示例 - sliceFromLiteral 的行为符合预期。

package main

import "fmt"

func create(iterations int) []int {
    a := make([]int, 0)
    for i := 0; i < iterations; i++ {
        a = append(a, i)
    }
    return a
}

func main() {
    sliceFromLoop()
    sliceFromLiteral()

}

func sliceFromLoop() {
    fmt.Printf("** NOT working as expected: **\n\n")
    i := create(11)
    fmt.Println("initial slice: ", i)
    j := append(i, 100)
    g := append(i, 101)
    h := append(i, 102)
    fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}

func sliceFromLiteral() {
    fmt.Printf("\n\n** working as expected: **\n")
    i := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    fmt.Println("initial slice: ", i)
    j := append(i, 100)
    g := append(i, 101)
    h := append(i, 102)
    fmt.Printf("i: %v\nj: %v\ng: %v\nh:%v\n", i, j, g, h)
}

play.golang 的链接: https://play.golang.org/p/INADVS3Ats

经过阅读、挖掘和实验,我发现这个问题源于slices 引用了相同的底层array 值,可以通过在添加任何内容之前将slice 复制到新值来解决,但是看起来很正常。 ..犹豫不决。

什么是基于旧切片创建许多新切片而不担心更改旧切片的值的惯用方式?

【问题讨论】:

    标签: go


    【解决方案1】:

    不要将append 分配给自身以外的任何东西。

    正如您在问题中提到的那样,混淆是由于append 既更改了基础数组又返回了一个新切片(因为长度可能会更改)。您可以想象它复制了该支持数组,但它没有,它只是分配一个新的 slice 指向它的对象。由于i 永远不会改变,所有这些追加都会不断将backingArray[12] 的值更改为不同的数字。

    将此与appending 对比为一个数组,它每次都分配一个新的文字数组。

    所以是的,您需要先复制切片,然后才能对其进行处理。

    func makeFromSlice(sl []int) []int {
        result := make([]int, len(sl))
        copy(result, sl)
        return result
    }
    
    func main() {
        i := make([]int, 0)
        for ii:=0; ii<11; ii++ {
            i = append(i, ii)
        }
        j := append(makeFromSlice(i), 100)  // works fine
    }
    

    解释切片文字行为是因为如果追加超出后备数组的cap,则会分配一个新数组。这与切片文字无关,而与超出上限的内部机制有关。

    a := []int{1,2,3,4,5,6,7}
    fmt.Printf("len(a) %d, cap(a) %d\n", a, len(a), cap(a))
    // len(a) 7, cap(a) 7
    
    b := make([]int, 0)
    for i:=1; i<8, i++ {
        b = append(b, i)
    }  // b := []int{1,2,3,4,5,6,7}
    // len(b) 7, cap(b) 8
    
    b = append(b, 1)  // any number, just so it hits cap
    
    i := append(b, 100)
    j := append(b, 101)
    k := append(b, 102)  // these work as expected now
    

    【讨论】:

    【解决方案2】:

    如果您需要切片的副本,除了复制切片之外别无他法。您几乎不应该将append 的结果分配给除append 的第一个参数以外的变量。它会导致很难找到错误,并且会根据切片是否具有所需的容量而表现不同。

    这不是一个通常需要的模式,但与所有这种性质的事情一样,如果您需要多次重复几行代码,那么您可以使用一个小的帮助函数:

    func copyAndAppend(i []int, vals ...int) []int {
        j := make([]int, len(i), len(i)+len(vals))
        copy(j, i)
        return append(j, vals...)
    }
    

    https://play.golang.org/p/J99_xEbaWo

    【讨论】:

      【解决方案3】:

      还有一个更简单的方法来实现copyAndAppend函数:

      func copyAndAppend(source []string, items ...string) []string {
          l := len(source)
          return append(source[:l:l], items...)
      }
      

      这里我们只是确保源没有可用容量,因此强制复制。

      【讨论】:

      最近更新 更多