golang slice

关于golang slice有很多大神写了很多文章,阐述了slice的底层实现和使用中注意点.这篇文章是我参考https://www.calhoun.io/why-are-slices-sometimes-altered-when-passed-by-value-in-go/ 加了一些自己的总结。

  • slice的实现原理,理解slice的实现原理是理解slice的一些很奇葩注意点的关键.一图胜千言.

《golang slice》 image.png

如图所示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的值没有变,但是数组元素被修改了。

    原文作者:myonlyzzy
    原文地址: https://www.jianshu.com/p/fb88137a24af
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞