从切片中删除元素时出现意外结果

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

Unexpected result when removing an element from a slice

问题

我在从切片中删除第一个元素时遇到了意外的结果,这是我的测试代码,希望能帮助你理解我的困惑。

首先,这是定义的结构体:

type A struct {
	member int
}

func (a A) String() string {
	return fmt.Sprintf("%v", a.member)
}

type B struct {
	a      A
	aPoint *A
}

func (b B) String() string {
	return fmt.Sprintf("a: %v, aPoint: %v", b.a, b.aPoint)
}

下面是测试用例:

func TestSliceRemoveFirstEle(t *testing.T) {
	demo := []B{
		{a: A{member: 1}},
		{a: A{member: 2}},
		{a: A{member: 3}},
	}

	for i := range demo {
		demo[i].aPoint = &demo[i].a
	}

	fmt.Println("操作前的 demo 是", demo) // 结果:操作前的 demo 是 [a: 1, aPoint: 1 a: 2, aPoint:2 a: 3, aPoint: 3]
	demo = append(demo[:0], demo[1:]...)
	fmt.Println("操作后的 demo 是", demo) // 结果:操作后的 demo 是 [a: 2, aPoint: 3 a: 3, aPoint: 3]
}

我期望的结果是 [a: 2, aPoint: 2 a: 3, aPoint: 3]

在第一个测试用例中,从切片中删除元素后出现了意外的结果。而在第二个测试用例中,通过另一种方式向切片中添加元素,结果是正确的。

第一个测试用例中的问题在于,当我们执行 demo = append(demo[:0], demo[1:]...) 时,虽然成功删除了第一个元素,但是切片中的每个元素的 aPoint 字段仍然指向了原来的地址。这是因为在 for 循环中,我们为每个元素的 aPoint 字段赋值时,使用的是 &demo[i].a,即取地址操作。而在删除元素后,切片中的元素地址发生了变化,导致 aPoint 字段指向了错误的地址。

而在第二个测试用例中,我们使用 make 函数创建了一个切片,并通过 append 函数向切片中添加元素。这样做的好处是,我们可以确保每个元素的地址是独立的,不会受到切片的变化影响。

因此,第一个测试用例中的操作导致了意外的结果,而第二个测试用例中的操作是正确的。

英文:

I encountered unexpected results when I remove the first element from slice, this is my test code, hope to help you understand my confuse
define structure

type A struct {
	member int
}

func (a A) String() string {
	return fmt.Sprintf("%v", a.member)
}

type B struct {
	a      A
	aPoint *A
}

func (b B) String() string {
	return fmt.Sprintf("a: %v, aPoint: %v", b.a, b.aPoint)
}

below is my test case

func TestSliceRemoveFirstEle(t *testing.T) {
	demo := []B{
		{a: A{member: 1}},
		{a: A{member: 2}},
		{a: A{member: 3}},
	}

	for i := range demo {
		demo[i].aPoint = &demo[i].a
	}

	fmt.Println("demo before operation is ", demo) // result: demo before operation is  [a: 1, aPoint: 1 a: 2, aPoint:2 a: 3, aPoint: 3]
	demo = append(demo[:0], demo[1:]...)
	fmt.Println("demo after operation is ", demo) // result: demo after operation is  [a: 2, aPoint: 3 a: 3, aPoint: 3]
}

my expected result is [a: 2, aPoint: 2 a: 3, aPoint: 3]

when I use another way adding element to the slice, it work well.

func TestAddSliceRemoveFirstEle(t *testing.T) {
	demo := make([]B, 0, 3)
	a1 := A{member: 1}
	a2 := A{member: 2}
	a3 := A{member: 3}
	demo = append(demo, B{a: a1, aPoint: &a1}, B{a: a2, aPoint: &a2}, B{a: a3, aPoint: &a3})

	fmt.Println("demo before operation is ", demo) // result: demo before operation is  [a: 1, aPoint: 1 a: 2, aPoint: 2 a: 3, aPoint: 3]
	demo = append(demo[:0], demo[1:]...)
	fmt.Println("demo after operation is ", demo) // result: demo after operation is  [a: 2, aPoint: 2 a: 3, aPoint: 3]
}

I'm confused about the result, what happened in the first case after removing element from slice, and what's the difference between these two implementations?

答案1

得分: 0

基本上,aPoint指针仍然指向相同的地址,但是这些地址上的值已经改变。

在追加之后,demo[0].aPoint == &demo[1].a,而不是demo[0].aPoint == &demo[0].a

slice表达式 demo[:0] 将生成一个长度为0的新切片,它指向与demo切片相同的底层数组。存储在该数组中的元素仍然存在,它们没有被丢弃。

因此,在demo[:0]之后,底层数组仍然是:

[
    B{a:1,<pointer_to_idx:0_field_a>},
    B{a:2,<pointer_to_idx:1_field_a>},
    B{a:3,<pointer_to_idx:2_field_a>},
]

demo[1:]... 删除了第一个元素,并将剩下的两个元素传递给append。然后,append接受这两个元素并更新底层数组的前两个元素。之后,底层数组将如下所示:

[
    B{a:2,<pointer_to_idx:1_field_a>},
    B{a:3,<pointer_to_idx:2_field_a>},
    B{a:3,<pointer_to_idx:2_field_a>},
]

请注意,现在,数组的第0个元素的aPointer字段指向第1个元素的a字段。而第1个元素的aPointer字段指向第2个元素的a字段。

英文:

Essentially the aPoint pointers still point to the same address, but the values at those addresses changed.

After the append it is demo[0].aPoint == &amp;demo[1].a and not demo[0].aPoint == &amp;demo[0].a.

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


The slice expression demo[:0] will result in a new slice of length 0 that points to the same underlying array as the demo slice. The elements stored in that array are still there, they are not discarded.

So after demo[:0] the underlying array is still:

[
    B{a:1,&lt;pointer_to_idx:0_field_a&gt;},
    B{a:2,&lt;pointer_to_idx:1_field_a&gt;},
    B{a:3,&lt;pointer_to_idx:2_field_a&gt;},
]

demo[1:]... drops the first element and passes the remaining two elements to append. Then append takes those two elements and updates the underlying array's first two elements. After that the underlying array will look like this:

[
    B{a:2,&lt;pointer_to_idx:1_field_a&gt;},
    B{a:3,&lt;pointer_to_idx:2_field_a&gt;},
    B{a:3,&lt;pointer_to_idx:2_field_a&gt;},
]

Notice that now, the aPointer field of the 0th element of the array points to the 1st element's a field. And the aPointer field of the 1st element points to the 2nd element's a field.

答案2

得分: 0

在你的第一个示例中,所有的A结构体都只存在于切片中,指针指向切片的元素。
在你的第二个示例中,A结构体存在于切片之外,因此切片的元素没有指针。

根据golang的append规范(https://golang.org/ref/spec#Appending_and_copying_slices):

> 如果切片s的容量不足以容纳额外的值,append会分配一个新的足够大的底层数组,该数组同时适应现有的切片元素和额外的值。否则,append会重用底层数组。

由于我们减小了大小,底层数组足够大,所以我们正在重用它。
这是容量为3的底层数组在重新切片之前和之后的样子:

123 - 之前
233 - 之后

在你的示例中,之前指向'2'的指针现在指向'3',一个更简单的方法是通过这个playground来观察:

package main

import (
	"fmt"
)

func main() {
	mySlice := []int{1, 2, 3}
	one := &mySlice[0]
	two := &mySlice[1]
	three := &mySlice[2]
	fmt.Printf("%v-%d\n%v-%d\n%v-%d\n",one,*one,two,*two,three,*three)
	fmt.Printf("%v cap %d\n",mySlice, cap(mySlice))
	mySlice = append(mySlice[:1],mySlice[2:]...)
	fmt.Printf("%v cap %d\n",mySlice, cap(mySlice))
	fmt.Printf("%v-%d\n%v-%d\n%v-%d\n",one,*one,two,*two,three,*three)
}

你会看到:

0xc0000be000-1
0xc0000be008-2
0xc0000be010-3
[1 2 3] cap 3
[1 3] cap 3
0xc0000be000-1
0xc0000be008-3
0xc0000be010-3

简而言之,从切片的元素中取出指针并重新切片将始终导致这种类型的错误。
根据这里的注释,它还可能导致内存泄漏问题:https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order

英文:

In your first example all the A structs exist only in the slice and the pointer points to elements of the slice.
In your second example, the A structs exist outside the slice and so there are no pointers against the elements of the slice.

As per the golang append specification at https://golang.org/ref/spec#Appending_and_copying_slices

> If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values. Otherwise, append re-uses the underlying array.

Since we are decreasing the size the underlying array is big enough so we are reusing it.
This is what the underlying array of capacity 3 looks like before and after the reslicing

123 - before
233 - after

What pointed at '2' before is now pointing at '3' as your example shows, a simpler way to see can be through this playground

package main

import (
	&quot;fmt&quot;
)

func main() {
	mySlice := []int{1, 2, 3}
	one := &amp;mySlice[0]
	two := &amp;mySlice[1]
	three := &amp;mySlice[2]
	fmt.Printf(&quot;%v-%d\n%v-%d\n%v-%d\n&quot;,one,*one,two,*two,three,*three)
	fmt.Printf(&quot;%v cap %d\n&quot;,mySlice, cap(mySlice))
	mySlice = append(mySlice[:1],mySlice[2:]...)
	fmt.Printf(&quot;%v cap %d\n&quot;,mySlice, cap(mySlice))
	fmt.Printf(&quot;%v-%d\n%v-%d\n%v-%d\n&quot;,one,*one,two,*two,three,*three)
}

Where you'll see

0xc0000be000-1
0xc0000be008-2
0xc0000be010-3
[1 2 3] cap 3
[1 3] cap 3
0xc0000be000-1
0xc0000be008-3
0xc0000be010-3

In short, taking pointers out of elements of a slice and reslicing it will always cause these kind of errors.
It could also cause memory leak issues as per the footnote here https://github.com/golang/go/wiki/SliceTricks#delete-without-preserving-order

huangapple
  • 本文由 发表于 2021年8月8日 12:38:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/68697830.html
匿名

发表评论

匿名网友

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

确定