一,切片传参
切片在工作中使用频率很高,但是不谨慎使用的话,很容易踩坑,特别是在传参的时候。话不多说,先看一段代码:
func main() {
slice := make([]int, 1, 3)
fmt.Printf("before,slice %v, addr is %p \n", slice, &slice)
changeSlice(slice)
fmt.Printf("after,slice %v, addr is %p \n", slice, &slice)
}
func changeSlice(slice2 []int) {
slice2 = append(slice2, 5)
slice2[0] = 1
fmt.Printf("during,slice2 %v, addr is %p \n", slice2, &slice2)
}
想想输出结果是什么?
before,slice [0], addr is 0xc000084020
during,slice2 [1 5], addr is 0xc000084060
after,slice [1], addr is 0xc000084020
从输出的第二段可以知道,在changeSlice方法中的切片地址和main方法中的切片地址不一样,所以可以看出,切片在作为参数进行传递的时候,形参对实参进行了拷贝,
是值传递而不是引用传递,然后观察第三行输出,执行changeSlice方法后slice被改变了,第一个元素从0变成了1,这是为哈?因为切片中包含三个参数,分别是:
- 长度:表示当前包含的元素总数。
- 容量:即配置的内存位置总数。
- 指向数据序列的指针
slice和slice2的指针是指向同一个数组的,(如果不是很清楚go中数组和切片的关系,可以看下这位同学的文章https://studygolang.com/articles/21543#reply0),changeSlice方法改变了它们指向的底层数组的第一个元素,
所以after中的slice的第一个元素也被改变了。
二,append方法中的坑
清楚了原因之后,我们对上述的代码进行一些改动,再多加两个元素:
func main() {
slice := make([]int, 1, 3)
fmt.Printf("before,slice %v, addr is %p \n", slice, &slice)
changeSlice(slice)
fmt.Printf("after,slice %v, addr is %p \n", slice, &slice)
}
func changeSlice(slice2 []int) {
slice2 = append(slice2, 5, 6, 7)
slice2[0] = 1
fmt.Printf("during,slice2 %v, addr is %p \n", slice2, &slice2)
}
再思考一下输出会是怎么样的?
before,slice [0], addr is 0xc000084020
during,slice2 [1 5 6 7], addr is 0xc000084060
after,slice [0], addr is 0xc000084020
好像和第一次输出没有差别,但仔细看下第三行输出,会发现和之前的输出不一样的,slice第一个元素并没有被改变,这又是为哈?
这里就是append方法的坑了
切片在创建时,底层匿名数组也随之被创建,数组长度等于切片的容量,当使用append方法追加元素的时候,如果追加的元素数量小于切片的剩余容量,那么就直接在切片尾部进行追加,
一点毛病没有,但是如果超出了切片的剩余容量,那么就要对底层的数组进行拷贝和扩容了。
此时append的原理大致如下:
1,新建一个切片s,s的长度和原切片一样,但是容量是原来的两倍,新建切片的时候自然会新建底层数组。
2,将原数组的元素拷贝到新的数组,也就是把原切片中的元素拷贝到s中。
3,把s赋值给原切片,然后进行小于剩余容量的append操作。
所以,这下我们就明白为什么以上输出的第三行slice的第一个元素没有被改变了,因为slice2指向的底层数组和slice的不一样了。
三,结论
从上面我们可以看出,切片传参使用不当的话,可能真的会踩坑,所以涉及到切片追加的场景,最好还是将改变后的切片return,防止出现数据丢失的情况。
go语言冯小白,若上述有错误之处,还请指出。
有疑问加站长微信联系(非本文作者)