陷阱:传递slice并尝试使用append来修改
[TOC]
实验与总结
append触发扩容底层数组改变,导致添加元素失败
append未触发扩容,添加元素成功,但是len字段值未被修改
应该在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。
案例
reference
最后更新于
这有帮助吗?