关于golang slice有很多大神写了很多文章,阐述了slice的底层实现和使用中注意点.这篇文章是我参考https://www.calhoun.io/why-are-slices-sometimes-altered-when-passed-by-value-in-go/ 加了一些自己的总结。
- slice的实现原理,理解slice的实现原理是理解slice的一些很奇葩注意点的关键.一图胜千言.
如图所示slice的底层是一个array. slice是一个结构里面有2属性 len 和cap分别表示当前使用的个数和可以容纳的个数.
- example1
func main() {
var s []int
for i:=1;i<7;i++{
s=append(s, i)
//fmt.Printf("%p\n",s)
}
s2:=s[:]
s1:=s[:]
s1=append(s1,100)
fmt.Printf("s1:%p len:%d,cap:%d\n",s1, len(s1), cap(s1))
s2=append(s2, 100,200,300)
fmt.Printf("s:%p len:%d,cap:%d\n",s, len(s), cap(s))
fmt.Printf("s2:%p len:%d,cap:%d\n",s2, len(s2), cap(s2))
}
如上代码,先声明一len 0 cap 0的slice 然后往里面append元素.s的len和cap大概如下变化
len cap s[i] s
0 0 0
1 1 1 0xc42001a050
2 2 2 0xc42001a070
3 4 3 0xc420016140
4 4 4 0xc420016140
5 8 5 0xc420018100
6 8 6 0xc420018100
这个变化就是当cap不够时重新分配一个新的cap为当前cap的2倍的array. 注意右边的s指向的地址变化,每当重新分配新的array的时候s 指向的地址就会变化。
回到上面的main代码,如果使用切片生成一个新的slice s1 s2,注意这里s1 append 了一个元素,s2 append 3个元素.
s1:0xc420018100 len:7,cap:8
s:0xc420018100 len:6,cap:8
s2:0xc42008c000 len:9,cap:16
Process finished with exit code 0
这里s1指向的地址和s一样,只是len加1,因为s1只是append了一个元素,没有超出s的cap
s2 指向的地址和s不一样了,append3个元素超出了s的cap所以重新分配了一个数组
- example2
func main() {
var s []int
for i:=1;i<7;i++{
s=append(s, i)
}
fmt.Printf("s point:%p s len:%d s cap:%d %v\n",s,s, len(s), cap(s))
reverse1(s)
fmt.Printf("s point:%p s len:%d s cap:%d %v\n",s,s, len(s), cap(s))
}
func reverse1(s[]int){
for i,j:=0,len(s);i<j;i++{
j=len(s)-(i+1)
s[i],s[j]=s[j],s[i]
}
}
func reverse2(s[]int){
s=append(s, 8888,7777)
for i,j:=0,len(s);i<j;i++{
j=len(s)-(i+1)
s[i],s[j]=s[j],s[i]
}
}
func reverse3(s[]int){
s=append(s, 8888,7777,1000)
for i,j:=0,len(s);i<j;i++{
j=len(s)-(i+1)
s[i],s[j]=s[j],s[i]
}
}
reverse1的输出
s point:0xc420018100 s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc420018100 s len:[6 5 4 3 2 1] s cap:6 8
reverse2 的输出
s point:0xc420018100 s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc420018100 s len:[7777 8888 6 5 4 3] s cap:6 8
reverse3 的输出
s point:0xc42008a040 s len:[1 2 3 4 5 6] s cap:6 8
s point:0xc42008a040 s len:[1 2 3 4 5 6] s cap:6 8
注意调用这3个函数后s的变化的差别
1. reverse1 只是将s中的元素顺序逆转了一下,只改变了元素的值。没有改变len cap.
2. reverse2 中给s append了一个元素,并且append之后的slice长度还是小于cap所有没有重新分配数组。注意这里append元素是不会改变main里面的s的len和cap的.
3 .reverse3 中append 3个元素超过cap,重新分配底层数组,所以reverse中的s2 指向的地址和main中的s分别指向2个不同的数组。所以逆转操作是发生在另外一个数组元素上而不是main的传进来的slice对应的数组上。
- golang中的值传递
golang官方中明确指出golang中的所有的函数传参都是值传递.什么意思,即所有的传进函数方法的参数都是原来变量的拷贝.包括slice也是,有人说slice map channel是引用传递,其实是错误的,但是可以在函数中修改传入的参数的内容啊。
如同刚才的slice的例子,传进来的s只是main总的一份拷贝, reverse1 中的s和main中的s得内存地址是不一样的。
s point:0xc42008a040 s len:[1 2 3 4 5 6] s cap:6 8
0xc420096020
0xc420096080
s point:0xc42008a040 s len:[6 5 4 3 2 1] s cap:6 8
只是slice里面指向的底层数组地址是同一个,所以reverse里面不管怎么改动,main里面的len cap pointer 都是不会被改变的。你只能改变pointer指向的数组的元素值
- 相似的类型
type A struct {
Ptr1 *B
Ptr2 *B
Val B
}
type B struct {
Str string
}
func main() {
a := A{
Ptr1: &B{"ptr-str-1"},
Ptr2: &B{"ptr-str-2"},
Val: B{"val-str"},
}
fmt.Println(a.Ptr1)
fmt.Println(a.Ptr2)
fmt.Println(a.Val)
demo(a)
fmt.Println(a.Ptr1)
fmt.Println(a.Ptr2)
fmt.Println(a.Val)
}
func demo(a A) {
// Update a value of a pointer and changes will persist
a.Ptr1.Str = "new-ptr-str1"
// Use an entirely new B object and changes won't persist
a.Ptr2 = &B{"new-ptr-str-2"}
a.Val.Str = "new-val-str"
}
其实slice是类似一个上面结构的类型
type slice struct {
array unsafe.Pointer
len int
cap int
}
上面的例子里面a作为值传递传给demo函数.demo函数持有的实际上是a的一份拷贝.修改这个拷贝里面的属性值是不会影响main里面的a的值,但是注意和slice类似,因为A是一个拥有一个指针属性的,虽然无法修改ptr1的值但是可以修改ptr1指针指向的地址的内容。所以ptr1 ptr2 中的地址还是没有变,但这个地址指向的内存中的内容被修改了。类似于slice里面pointer的值没有变,但是数组元素被修改了。
有疑问加站长微信联系(非本文作者)