美文网首页
golang slice

golang slice

作者: myonlyzzy | 来源:发表于2018-06-29 14:21 被阅读0次

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

    • slice的实现原理,理解slice的实现原理是理解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的值没有变,但是数组元素被修改了。

    相关文章

      网友评论

          本文标题:golang slice

          本文链接:https://www.haomeiwen.com/subject/dibjeftx.html