陷阱:传递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源码 有定义如下结构:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

并且在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

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

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

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

思考

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

package main

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

func main() {
    // 4. 思考
    var mySlice = make([]int, 0, 10)
    handle(mySlice)
    var shStart = (*reflect.SliceHeader)(unsafe.Pointer(&mySlice))
    fmt.Printf("底层数组的append前第一个元素是:%d \n", *(*int)(unsafe.Pointer(shStart.Data)))
    mySlice = append(mySlice, 525)
    fmt.Printf("底层数组的append后第一个元素是:%d \n", *(*int)(unsafe.Pointer(shStart.Data)))
    fmt.Printf("mySlice: %v \n", mySlice)
    //调用函数前 Data:824633835680,Len:0,Cap:10
    //函数内Append前 Data:824633835680,Len:0,Cap:10
    //底层数组的第一个元素修改前:0 
    //函数内Append后 Data:824633835680,Len:1,Cap:10
    //底层数组的第一个元素修改后:1024 
    //调用函数后 Data:824633835680,Len:0,Cap:10
    //
    //底层数组的append前第一个元素是:1024 
    //底层数组的append后第一个元素是:525 
    //mySlice: [525] 
}

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)
    fmt.Printf("底层数组的第一个元素修改前:%d \n", *(*int)(unsafe.Pointer(shStart.Data)))
    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)
    fmt.Printf("底层数组的第一个元素修改后:%d \n", *(*int)(unsafe.Pointer(shStart.Data)))
}

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

案例

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

reference

【Go】深入剖析slice和array

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

最后更新于

这有帮助吗?