英文:
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 == &demo[1].a
and not demo[0].aPoint == &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,<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:]...
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,<pointer_to_idx:1_field_a>},
B{a:3,<pointer_to_idx:2_field_a>},
B{a:3,<pointer_to_idx:2_field_a>},
]
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 (
"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)
}
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论