陷阱:传递slice并尝试使用append来修改

[TOC]

实验与总结

  1. append触发扩容底层数组改变,导致添加元素失败

  2. append未触发扩容,添加元素成功,但是len字段值未被修改

  3. 应该在append之后将值返回,类似于go官方append的做法slice = append(slice, value)

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    // 1. append触发扩容底层数组改变,导致添加元素失败
    handle(make([]int, 0, 0))
    //调用函数前 Data:824634175272,Len:0,Cap:0
    //函数内Append前 Data:824634175272,Len:0,Cap:0
    //函数内Append后 Data:824633819240,Len:1,Cap:1
    //调用函数后 Data:824634175272,Len:0,Cap:0

    // 2. append未触发扩容,添加元素成功,但是len字段值未被修改
    handle(make([]int, 0, 10))
    //调用函数前 Data:824634175272,Len:0,Cap:10
    //函数内Append前 Data:824634175272,Len:0,Cap:10
    //函数内Append后 Data:824634175272,Len:1,Cap:10
    //调用函数后 Data:824634175272,Len:0,Cap:10

    // 3. 应该在append之后将值返回
    var list = make([]int, 0)
    list = goodHandle(list)
    //调用函数前 Data:18533560,Len:0,Cap:0
    //函数内Append前 Data:18533560,Len:0,Cap:0
    //函数内Append后 Data:824633819368,Len:1,Cap:1
    //调用函数后 Data:824633819368,Len:1,Cap:1
}

func goodHandle(list []int) []int {
    var shStart = (*reflect.SliceHeader)(unsafe.Pointer(&list))
    fmt.Printf("调用函数前 Data:%d,Len:%d,Cap:%d\n", shStart.Data, shStart.Len, shStart.Cap)
    list = goodAppend(list)
    var shEnd = (*reflect.SliceHeader)(unsafe.Pointer(&list))
    reflect.ValueOf(list)
    fmt.Printf("调用函数后 Data:%d,Len:%d,Cap:%d\n", shEnd.Data, shEnd.Len, shEnd.Cap)
    fmt.Println()
    return list
}

func goodAppend(v []int) []int {
    var shStart = (*reflect.SliceHeader)(unsafe.Pointer(&v))
    fmt.Printf("函数内Append前 Data:%d,Len:%d,Cap:%d\n", shStart.Data, shStart.Len, shStart.Cap)
    v = append(v, 1024)
    var shEnd = (*reflect.SliceHeader)(unsafe.Pointer(&v))
    fmt.Printf("函数内Append后 Data:%d,Len:%d,Cap:%d\n", shEnd.Data, shEnd.Len, shEnd.Cap)
    return v
}

// append触发扩容底层数组改变导致添加元素失效
func handle(list []int) {
    var shStart = (*reflect.SliceHeader)(unsafe.Pointer(&list))
    fmt.Printf("调用函数前 Data:%d,Len:%d,Cap:%d\n", shStart.Data, shStart.Len, shStart.Cap)
    Append(list)
    var shEnd = (*reflect.SliceHeader)(unsafe.Pointer(&list))
    reflect.ValueOf(list)
    fmt.Printf("调用函数后 Data:%d,Len:%d,Cap:%d\n", shEnd.Data, shEnd.Len, shEnd.Cap)
    fmt.Println()
}

func Append(v []int) {
    var shStart = (*reflect.SliceHeader)(unsafe.Pointer(&v))
    fmt.Printf("函数内Append前 Data:%d,Len:%d,Cap:%d\n", shStart.Data, shStart.Len, shStart.Cap)
    v = append(v, 1024)
    var shEnd = (*reflect.SliceHeader)(unsafe.Pointer(&v))
    fmt.Printf("函数内Append后 Data:%d,Len:%d,Cap:%d\n", shEnd.Data, shEnd.Len, shEnd.Cap)
}

分析

slice源码 有定义如下结构:

并且在append时如果触发扩容growslice func growslice(et *_type, old slice, cap int) slice将获得新的slice结构体对象。 我们大致可以知道切片slice是一个结构体,引用了一个底层数组array, 以cap表示当前切片的容量,以len 表示当前切片中存储元素的个数。

makeslice(et *_type, len, cap int) unsafe.Pointer的角度看切片slice又是type Pointer *ArbitraryType。 在 源码 Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer. 得知也可以把它看作是 SliceHeader

所以添加元素的时候如果容量不够用则会触发扩容,那么会另外再生成一个数组,函数内变量的array字段指向了新的数组,但由于是原slice结构体的副本,所以修改对原变量并不生效,因此添加失败。

同样在添加元素的时候尽管容量够用,但是修改函数内副本的len字段对原变量并不生效,也会添加元素失败,毕竟当我们尝试通过下标访问最后一个元素的时候会越界。

思考

底层数组是指针,那么副本拷贝的也是指针,这样在容量足够的情况下理论上数据是添加到底层数组中了,那么我们对原变量执行append后,通过下标获取的元素是之前在函数内添加的还是当前append添加的呢?

获得的值是525而不是handle函数中写入的1024,为什么?? 原因是我们当前的len是0,执行append在操作底层数组的时候,操作类似于array[0]=525; len++ 会把之前函数中append的1024覆盖掉,所以获得的值是525。

案例

Go 切片这道题,吵了一个下午!

reference

【Go】深入剖析slice和array

Go 切片这道题,吵了一个下午!

最后更新于

这有帮助吗?