转载公众号:lysnow_org

留言起因

问题: 考虑将slice这种引用类型作为自定义接受者,并绑定方法如下,此时的slice空间容量足够,调用方法前后其地址并不会改变,那么为何append后的切片内部成员不会改变? 默认拷贝的副本是slice引用,应该要能修改或者添加成员才符合预期的。

type Slice []int

func (A Slice)Append(value int) {
    A = append(A, value)
}

func main() {
    mSlice := make(Slice, 5, 10)
    mSlice.Append(5)
    fmt.Println(mSlice)
}

打印结果

[0, 0, 0, 0, 0]

由于值拷贝的原因,Append方法前后的切片唯一有关联的就是底层指向的数组,打印结果不一样就是因为原来切片太短了。这个也可以在执行完Append方法后,生成一个新的切片(长度大于5)并打印验证。

问题分析

从以上的输出打印中,我们的确可以看到mSlice并没有任何变化,就是方法Append没有起任何作用。既然Slice是引用类型,修改了指向应该也会跟着改,其实我们知道,这个修改引用的指向是在Append方法内的,离开就不起作用了。

其实以上都不是根本,append后的Slice已经不是原来的Slice了。这时候有的朋友可能又疑惑了,append返回的Slice的指针和原Slice的指针一样的啊,怎么会不是一个呢?我们来测试一次,修改代码如下:

func (A Slice)Append(value int) {
    A1 := append(A, value)
    fmt.Printf("%p\n%p\n",A,A1)
}

我们用A1存储append方法返回的Slice,然后打印返回A1和原A的指针地址,发现的确一样(这里打印的指针地址是A和A1指向的底层ScliceHeader的Data指针地址,而不是变量直接取地址)。大家可以自己运行试试。其实我们自己在make一个Slice的时候会发现,是可以有三个参数的,一个是数据、一个是长度、一个是容量,也就是说,Slice是这样的一个结构,现在该是我们的SliceHeader登场的时候了。

SliceHeader登场

SliceHeader是Slice运行时的具体表现,它的结构定义如下:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

正好对应Slice的三要素,Data指向具体的底层数据源数组,Len代表长度,Cap代表容量。

既然Slice就是SliceHeader,那么我们把Slice转化为SliceHeader,来看看A和A1内部具体的字段值,这样来判断他们是否一致,我们修改Append方法如下:

func (A Slice)Append(value int) {
    A1 := append(A, value)

    sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
    fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

    sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
    fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}

通过unsafe.Pointer指针进行强制类型转换,都转换为*reflect.SliceHeader类型后,我们分别输出他们的Data、Len、Cap字段,现在我们看看输出的结果。

A  Data:824634204160,Len:10,Cap:20
A1 Data:824634204160,Len:11,Cap:20

这下大家明白了吧,他们的Len不一样,并不是一个Slice,所以使用append方法并没有改变原来的A,而是新生成了一个A1,即使Dreamerque这位朋友通过如下代码 A = append(A, value) 进行复制,也只是一个mSlice的拷贝A的指向被改变了,而且这个A只在Append方法内有效,mSlice本身并没有改变,所以输出的mSlice不会有任何变化。

这里正确的做法是让Append返回append后的结果。其实对于内置函数append的使用,Go语言(golang)官方做了说明的,要保存返回的值。

Append returns the updated slice. It is therefore necessary to store the result of append

以上Dreamerque这位朋友的例子中,设置的Len是10,Cap是20,因为Cap足够大,所以内置函数append并没有生成新的底层数组,现在我们把Cap改为10。

type Slice []int

func (A Slice)Append(value int) {
    A1 := append(A, value)

    sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
    fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

    sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
    fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}

func main() {
    mSlice := make(Slice, 10, 10)
    mSlice.Append(5)
    fmt.Println(mSlice)
}

运行代码我们会发现两个Slice的Data不再一样了。

A  Data:824633835680,Len:10,Cap:10
A1 Data:824634204160,Len:11,Cap:20

这是因为在append的时候,发现Cap不够,生成了一个新的Data数组,用于存储新的数据,并且同时扩充了Cap容量。