Go语言中Slice详解

Slice常见操作及底层原理实现

一 什么是Slice

  1. slice(切片)是一种数组结构,相当于是一个动态的数组,可以按需自动增长和缩小。
  2. 那么为什么需要slice呢?
    • 在GO语言中,数组是一个值,在进行传参和赋值操作时,都会将数组拷贝一份,当数组较大时耗费较多资源;使用数组的指针会较为麻烦
    • slice是引用类型,传参时不需要再用到指针;slice本质上是数组的指针,所以传参时不需要拷贝数组,耗费较小;可以动态改变数组大小,使用更加的方便

二 Slice的常用操作

  1. slice的创建

    • a. 使用内置的make函数

      //只指定长度,则默认容量和长度相等
      //容量的定义在后面解释
      slice := make([]string, 5)
      //指定长度和容量,容量不能小于长度
      slice = make([]string, 3, 5)
      
    • b. 使用切片字面量

      //其长度和容量都是3
      s1 := []string{"dog", "cat", "bear"}
      //使用索引声明切片
      //下面创建了一个长度为100的切片
      s2 := []int{99: 0}
      
    • c. nil和空切片

      • 声明时不做任何初始化就会创建一个nil切片

        var slice []int
        //new 产生的是指针,需要用* 
        slice := *new([]int) 
        
      • 声明空切片

        //使用make
        s1 := make([]int, 0)
        //使用切片字面量
        s2 := []int{}
        
      • 空切片与nil切片的区别:

        • nil切片=nil, 而空切片!=nil,在使用切片进行逻辑运算时尽量不要使用空切片
        • 空切片指针指向一个特殊的zerobase地址,而nil为0
        • 在JSON序列化有区别:nill切片为{“values":null}, 而空切片为{"value" []}
  2. 增加元素

    • 使用内置函数append添加元素
  3. 复制切片

    • 使用copy
  4. 删除元素,内置没有提供,下面简单实现一下:

    func deleteSlice(index int, s []int) []int{
     s1 := s[:index]
     s1 = append(s1, s[index+1:]...)
     return s1
    }
    

三 slice底层实现

  1. 切片是基于数组实现的,是数组的抽象,因此底层的内存是连续的,效率较高,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化的好处。切片对象本身的很小,是因为它是只有3个字段的数据结构:一个是指向底层数组的指针,一个是切片的长度,一个是切片的容量。这3个字段,就是Go语言操作底层数组的元数据。查看其源码如下:

    type notInHeapSlice struct {
     array *notInHeap        //底层数组的指针
     len   int              //切片的长度
     cap   int              //切片的容量
    }
    
    slice底层结构.png
  1. 关于容量与长度:

    • a. 长度:slice当前元素个数

      ​ 容量:底层数组的空间,当容量不足时会开辟新的数组空间,避免频繁开辟内存空间

    • b. 计算:对于底层数组容量是k的切片slice[i,j]来说

      • 长度:j-i
      • 容量:k-i
    • c. 数组索引不能超过长度

  1. 切片增长会改变长度,容量不一定,需要看可用容量,当容量不足时会分配一个新的底层数组,将现有的值复制到新数组再添加新的值。

     s := make([]int, 0, 5)
     t := append(s, 1,2,3)
     fmt.Printf("before_arr_s = %p\n", s)
     fmt.Println(t)
     fmt.Printf("before_arr_t = %p\n", t)
    
     t = append(t, 1,2,3)
     fmt.Println(s)
     fmt.Printf("after_arr_s = %p\n", s)
     fmt.Println(t)
     fmt.Printf("after_arr_t = %p\n", t)
    
    --------------------------
    []
    before_arr_s = 0xc000078030
    [1 2 3]
    before_arr_t = 0xc000078030
    []
    after_arr_s = 0xc000078030
    [1 2 3 1 2 3]
    //当第二次添加元素时超过了容量限制,于是重新开辟了数组,查看地址发现的确发生了改变
    after_arr_t = 0xc00008c000
    

四 拓展:三个索引的切片

  1. 第三个索引可以限定容量。对于slice[i:j:k],长度=j-i,容量=k-i

  2. 在创建切片时设置切片的容量和长度一样,可以强制让新切片的第一个append操作创建新的底层数组,与原有的底层数组分类。保持数组的简洁,更加的安全。

    • a. 若不限定分片的容量,直接append的话可能会覆盖底层数组,从而影响到其他切片,出现奇怪的bug
    func main(){
     b := []int{1, 2, 3, 4, 5, 6}
     c := b[: 2]
     c = append(c, 7)
     fmt.Println(b)
     fmt.Println(c)
    }
    
    ----------------------
    //b切片被c影响
    [1 2 7 4 5 6]
    [1 2 7]
    
    • b. 在使用切片时限定容量可以避免上述情况
    func main(){
     b := []int{1, 2, 3, 4, 5, 6}
     c := b[: 2:2]
     c = append(c, 7)
     fmt.Println(b)
     fmt.Println(c)
    }
    
    ------------------
    //在使用切片时限定容量,c切片append时开辟了新的数组,不影响原数组上的切片
    [1 2 3 4 5 6]
    [1 2 7]
    
    

更多Go的相关文章发布在我的个人博客上,欢迎访问
www.guiguiyo.cn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容