目录
其实真正会影响到我们编程结果的是,我们在我们调用的函数里面使用append函数,如果不传切片指针的话,在调用函数中使用append是没有效果的,下面看例子2和例子3的效果对比就知道了。
总结:go语言中切片不像其他语言中的数组那样,在函数中传参时是传的引用,而是传的值,而且切片还有些特殊的地方。接下来就从下面的例子中学习。
切片底层
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片底层其实是一个结构体,结构体中含有一个指向底层数组指针、切片长度,切片容量。所以在进行关于切片的传参的时候还是有一些比较有意思的事情会发生的。
例子1
func main() {
var arr = []int{1, 2, 3, 4, 5}
fmt.Printf("arr pointer: %p\n", &arr)
test(arr)
fmt.Printf("arr: %v\n", arr)
}
func test(data []int) {
fmt.Printf("data pointer: %p\n", &data)
data[0] = 100
}
结果:
arr pointer: 0xc000096060
data pointer: 0xc000096078
arr: [100 2 3 4 5]
可以看出来,两个地方打印的地址并不是相同的,但是test函数中对arr的修改的的确确是影响到了main函数中的arr切片,这会造成传递的是引用的假象。
真正原因是虽然是两个切片对象,但是切片对象中的数组指针指向了同一个数组,所以对其修改会影响到arr中原来的值。
其实真正会影响到我们编程结果的是,我们在我们调用的函数里面使用append函数,如果不传切片指针的话,在调用函数中使用append是没有效果的,下面看例子2和例子3的效果对比就知道了。
例子2
func main() {
var arr = []int{1, 2, 3, 4, 5}
fmt.Printf("arr pointer: %p\n", &arr)
test(arr)
fmt.Printf("arr: %v\n", arr)
}
func test(data []int) {
fmt.Printf("data pointer: %p\n", data)
data = append(data, 100)
}
结果:
arr pointer: 0xc000004078
data pointer: 0xc00000c3c0
arr: [1 2 3 4 5]
可以看到append函数没有起到追加的作用
例子3
func main() {
var arr = []int{1, 2, 3, 4, 5}
fmt.Printf("arr pointer: %p\n", &arr)
test(&arr)
fmt.Printf("arr: %v\n", arr)
}
func test(data *[]int) {
fmt.Printf("data pointer: %p\n", data)
*data = append(*data, 100)
(*data)[0] = 100
}
结果:
arr pointer: 0xc000004078
data pointer: 0xc000004078
arr: [100 2 3 4 5 100]
可以看到这次两个地址是一样的了,而且改动确实对arr数组本身有影响。
例子4
看下这个元素变量的所占空间大小,也能验证我们上面的这些实验。
func main() {
s1 := []int{1, 2, 3, 4, 5}
a1 := [5]int{1, 2, 3, 4, 5}
p1 := Person{}
p2 := &Person{}
p3 := new(Person)
m1 := make(map[string]int)
m2 := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
fmt.Println(unsafe.Sizeof(s1)) // 24 = 3 * 8
fmt.Println(unsafe.Sizeof(a1)) // 40
fmt.Println(unsafe.Sizeof(p1)) // 32 = 4 * 8
fmt.Println(unsafe.Sizeof(p2)) // 8
fmt.Println(unsafe.Sizeof(p3)) // 8
fmt.Println(unsafe.Sizeof(m1)) // 8
fmt.Println(unsafe.Sizeof(m2)) // 8
}
type Person struct{
attr1 int
attr2 int
attr3 int
attr4 int
}
由于在64位系统上,uintptr
和int
都是占用8字节,所以在这个例子中,p2、p3这两个指针所占空间都是8字节。
而结构体遍历会根据其所包含的属性来确定所占空间大小,因此,p1占32个字节(4*8)。
再看s1这个切片,它占24个字节(3 * 8),正好印证了前文所说的切片的底层结构是由三个成员属性组成的结构体这个说法。
最后看一下 m1 和 m2 这两个map,它们占用都是8字节,也就是说,它们其实是指针,所以map传参时,虽然也是值传递,但这个传递的值是指针,也就是地址值,在函数体中操作map会影响到函数外的map,所以看上去像是“引用传递”,其实究其根源,还是值传递。
所以,切片在传参时,是使用与结构体相同的传参方式,即传值
方式。而且,Go语言中也只有值传递,没有引用传递。
最后我们要思考的问题还有就是切片的len和cap的关系。我们在日常使用中如果用类似数组下标这种方式访问slice[index],其中index是不能超过切片的len的。但是我们每次append函数进行扩容时,是扩容的cap变量。我们用append函数时 是在slice[len(slice)]这个位置去追加元素。
参考:切片