slice实践以及底层原理
Topic:
- slice是传值还是传地址
- slice作为函数入参的各种坑
- 切片的数据结构
- 切片的创建
- 切片的扩容
- 切片的拷贝
- base golang 1.13
slice是传值还是传地址
先看测试例子1:
func testModifyElem(s []int) {
s[0] = 1
fmt.Printf("s inter address is %p \n", s)
fmt.Println("s=", s)
}
func main() {
fmt.Println("------------------------ slice0 -------------------------")
sa := make([]int, 10)
fmt.Printf("sa address is %p \n", sa)
fmt.Println("sa=", sa)
testModifyElem(sa)
fmt.Printf("post sa address is %p \n", sa)
fmt.Println("post sa=", sa)
}
输出结果是:
------------------------ slice0 -------------------------
sa address is 0xc00001e0a0
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc00001e0a0
s= [1 0 0 0 0 0 0 0 0 0]
post sa address is 0xc00001e0a0
post sa= [1 0 0 0 0 0 0 0 0 0]
上面的testModifyElem函数传递的是 slice 对象,输出结果可以看出两点:
- 函数内部对slice某个元素的修改,会影响函数外面的slice。
- 函数内部的slice地址和函数外部的slice是一致的。
这种现象会给人一种感觉,golang slice的传递是引用传递(实际上是错误的)。我们再来看一个例子:
func testModifyElem2(s []int) {
s[0] = 1
fmt.Printf("s inter address is %p \n", s)
fmt.Println("s=", s)
s = append(s, 11)
fmt.Printf("s inter address(after append) is %p \n", s)
fmt.Println("s=(after append)", s)
}
func main() {
fmt.Println("------------------------ slice0 -------------------------")
sa := make([]int, 10)
fmt.Printf("sa address is %p \n", sa)
fmt.Println("sa=", sa)
testModifyElem2(sa)
fmt.Printf("post sa address is %p \n", sa)
fmt.Println("post sa=", sa)
}
输出结果是:
------------------------ slice0 -------------------------
sa address is 0xc00009e000
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc00009e000
s= [1 0 0 0 0 0 0 0 0 0]
s inter address(after append) is 0xc0000b0000
s=(after append) [1 0 0 0 0 0 0 0 0 0 11]
post sa address is 0xc00009e000
post sa= [1 0 0 0 0 0 0 0 0 0]
从这个输出结果我们可以得到:
- 函数内部对slice某个元素的修改,会影响函数外面的slice。
- 如果函数内部对slice的append操作,不会影响到函数外部slice。
- 我们的slice len和cap都是10,函数内部的append会导致扩容,扩容之后,地址发生了变化,但是函数外部的slice地址并没有变化。这说明了golang的slice传递slice不是引用传递,而是值传递。
还有一种场景,如果slice在函数内部append不会导致扩容呢?看下面的例子:
func testModifyElem2(s []int) {
s[0] = 1
fmt.Printf("s inter address is %p \n", s)
fmt.Println("s=", s)
s = append(s, 11)
fmt.Printf("s inter address(after append) is %p \n", s)
fmt.Println("s=(after append)", s)
}
func main() {
fmt.Println("------------------------ slice0 -------------------------")
sa := make([]int, 10, 20)
fmt.Printf("sa address is %p \n", sa)
fmt.Println("sa=", sa)
testModifyElem2(sa)
fmt.Printf("post sa address is %p \n", sa)
fmt.Println("post sa=", sa)
}
运行结果如下:
------------------------ slice0 -------------------------
sa address is 0xc0000ac000
sa= [0 0 0 0 0 0 0 0 0 0]
s inter address is 0xc0000ac000
s= [1 0 0 0 0 0 0 0 0 0]
s inter address(after append) is 0xc0000ac000
s=(after append) [1 0 0 0 0 0 0 0 0 0 11]
post sa address is 0xc0000ac000
post sa= [1 0 0 0 0 0 0 0 0 0]
我们可以看到两点:
- 函数内部的append不会影响到外部的slice;
- 如果函数内部的append不会导致扩容,那么slice的地址也不会变化。
这个结论进一步验证了我们的结论。
所以我们的结论就是:值传递。
- slice的函数传参或者赋值,其实都是值传递,也就是会发生拷贝。slice内部是持有三个变量,实际存储数据的数组地址、当前slice的len以及slice的容量。
- 传参时,实际上会拷贝len、cap、以及底层实际存储数据的数组的首地址。
- 如果函数内部发生了append的操作,函数外部是感知不到的。
当我们调用函数 testMethod(s) 的时候,实际上类似于 testMethod(*addr, len, cap)。
其实我们在调用append函数的时候,用法都是 s = append(s, data...)
。 append函数实际上返回的是一个新的slice对象,然后新的对象的属性;len、cap以及底层数组首地址会拷贝赋值给s。
slice作为函数入参的各种坑
前面一章其实已经列出了slice传参的一些坑。这里再列出一些典型的坑。
Case1: 函数入参是一个slice, 期待函数内部的的修改,比如新增、删除、更改会在外部生效。
这里的实现有两种方法:传一个slice的指针进去;或者return入参的slice并且赋值给外部的slice。
(1)传指针, 直接看code:
func testSlicePtr(sp *[]int) {
fmt.Printf("sp address is %p \n", *sp)
fmt.Println("sp=", *sp)
*sp = append(*sp, 10)
fmt