意外的切片追加行为

huangapple go评论74阅读模式
英文:

unexpected slice append behaviour

问题

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

在这个特定的例子中,这意味着sliceFromLoopjgh切片的最后一个元素不是分别是100101102,而是始终是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)
}

在阅读、挖掘和实验之后,我发现这个问题是由于slices引用了相同的底层数组值而导致的,可以通过在追加任何内容之前将slice复制到新的slice中来解决,但这看起来相当犹豫不决。

有没有一种惯用的方法可以基于旧的切片创建许多新的切片,而不必担心更改旧切片的值?

英文:

I encountered weird behaviour in go code today: when I append elements to slice in loop and then try to create new slices based on the result of the loop, last append overrides slices from previous appends.

In this particular example it means that sliceFromLoop j,g and h slice's last element are not 100,101 and 102 respectively, but...always 102!

Second example - sliceFromLiteral behaves as expected.

package main

import &quot;fmt&quot;

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

func main() {
	sliceFromLoop()
	sliceFromLiteral()

}

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

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

link to play.golang:
https://play.golang.org/p/INADVS3Ats

After some reading, digging and experimenting I found that this problem is originated in slices referencing the same underlaying array values and can be solved by copying slice to new one before appending anything, however it looks quite... hesitantly.

What's the idomatic way for creating many new slices based on old ones and not worrying about changing values of old slices?

答案1

得分: 16

不要将append分配给除它自身以外的任何变量。

正如你在问题中提到的,混淆是因为append既改变了底层数组,又返回一个新的切片(因为长度可能会改变)。你可能会认为它会复制该后备数组,但实际上它只是分配一个指向它的新的slice对象。由于i从不改变,所有这些append都会将backingArray[12]的值更改为不同的数字。

与此相反,将元素append到数组时,每次都会分配一个新的字面数组。

所以是的,你需要在对其进行操作之前复制切片。

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)  // 正常工作
}

切片字面量的行为解释是因为如果append操作超过了后备数组的cap,则会分配一个新的数组。这与切片字面量无关,而与超过cap的内部工作原理有关。

a := []int{1, 2, 3, 4, 5, 6, 7}
fmt.Printf("len(a) %d, cap(a) %d\n", 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)  // 任意数字,只要达到cap

i := append(b, 100)
j := append(b, 101)
k := append(b, 102)  // 现在这些都按预期工作
英文:

Don't assign append to anything other than itself.

As you mention in the question, the confusion is due to the fact that append both changes the underlying array and returns a new slice (since the length might be changed). You'd imagine that it copies that backing array, but it doesn't, it just allocates a new slice object that points at it. Since i never changes, all those appends keep changing the value of backingArray[12] to a different number.

Contrast this to appending to an array, which allocates a new literal array every time.

So yes, you need to copy the slice before you can work on it.

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&lt;11; ii++ {
        i = append(i, ii)
    }
    j := append(makeFromSlice(i), 100)  // works fine
}

The slice literal behavior is explained because a new array is allocated if the append would exceed the cap of the backing array. This has nothing to do with slice literals and everything to do with the internals of how exceeding the cap works.

a := []int{1,2,3,4,5,6,7}
fmt.Printf(&quot;len(a) %d, cap(a) %d\n&quot;, a, len(a), cap(a))
// len(a) 7, cap(a) 7

b := make([]int, 0)
for i:=1; i&lt;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

得分: 5

如果你需要复制一个切片,除了复制切片之外没有其他方法。你几乎永远不应该将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

英文:

If you need a copy of a slice, there's no other way to do it other than, copying the slice. You should almost never assign the result of append to a variable other than the first argument of append. It leads to hard to find bugs, and will behave differently depending on whether the slice has the required capacity or not.

This isn't a commonly needed pattern, but as with all things of this nature if you need to repeate a few lines of code multiple times, then you can use a small helper function:

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

得分: 0

这里还有一种更简单的实现copyAndAppend函数的方法:

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

在这里,我们只需确保source没有可用的容量,这样就会强制进行复制。

英文:

There is also a little bit simpler way to implement copyAndAppend function:

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

Here we just make sure that source has no available capacity and so copying is forced.

huangapple
  • 本文由 发表于 2016年2月9日 01:48:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/35276022.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定