• Golang原理分析:切片(slice)原理及扩容机制


    在这里插入图片描述

    《Go语言精进之路》切片相关章节学习笔记及实验。

    1.切片原理

    说切片之前,先看看Go语言数组。Go数组是一个固定长度的、容纳同构类型元素的连续序列,因此Go数组类型具有两个属性:长度及类型:

    var a [1]int
    var b [2]byte
    var c [3]string
    
    • 1
    • 2
    • 3

    数组的特性是声明即定长,无法原地进行扩容,所以Golang中一般来说需要使用到数组的场合都会使用切片来替代。

    切片的特性:

    • 不定长,能随着存储容量进行自动扩容
    • 提供append函数,能够方便地添加值

    切片的本质是引用底层数组头指针+当前切片长度+底层数组大小:即array、len和cap:

    • $goroot/src/runtime/slice.go
    type slice struct {
       array unsafe.Pointer    // 底层数组头指针
       len   int               // 当前切片长度 
       cap   int               // 底层数组最大容量
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用时通过make关键字来进行初始化切片分配一个新的底层数组,或者是基于指定的数组进行切片化:

    • make关键字,创建一个长度为5的切片,如果没有指定cap参数则默认与长度一致为5(底层数组长度也为5)
    s := make([]byte,5)
    
    • 1

    在这里插入图片描述

    • 直接通过已有数组创建切片,array指向底层数组的指定位置作为头部,从头部开始计算len及cap:
    u := [10]byte{11,12,13,14,15,16,17,18,19,20} // 长度为10的数组
    s := u[3:7] // 从数组的第3个元素至第6个元素建立切片(前闭后开区间)
    
    • 1
    • 2

    在这里插入图片描述

    • 底层数组可以被多个切片共享,如果切片共享了底层数组同一值,那么切片1进行修改后,切片2读取该值时也会读取到修改的结果:
      在这里插入图片描述

    2.扩容机制

    切片能够在底层数组容量不够时进行扩容,每次append时如果容量不足,会创建2倍大小于老数组的新数组,并复制老数组数据至新的数组中:

    func main() {
            var s []int                // 初始化时,底层数组为数组为nil
            s = append(s, 11)          
            fmt.Println(len(s), cap(s)) // 1 1
            s = append(s, 12) 
            fmt.Println(len(s), cap(s)) // 2 2
            s = append(s, 13)
            fmt.Println(len(s), cap(s)) // 3 4
            s = append(s, 14)
            fmt.Println(len(s), cap(s)) // 4 4
            s = append(s, 15)
            fmt.Println(len(s), cap(s)) // 5 8
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    • 扩容存在损耗,所以尽可能在已知数据最长度的情况下声明切片的cap,避免频繁库容切换底层数组带来的性能损耗:
    func BenchmarkAppendZeroSlice(b *testing.B) {
       maxNum := 10000
       for i := 0; i < b.N; i++ {
           // 空切片
          var zeroSlice []int
          for j := 0; j < maxNum; j++ {
             zeroSlice = append(zeroSlice, j)
          }
       }
    }
    
    func BenchmarkAppendCapSlice(b *testing.B) {
       maxNum := 10000
       for i := 0; i < b.N; i++ {
           // 声明cap的切片
          capSlice := make([]int, 0, maxNum)
          for j := 0; j < maxNum; j++ {
             capSlice = append(capSlice, j)
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    从基准测试结果可以看出,声明了cap的切片能避免频繁创建底层数组及复制,单轮append时间为18847ns,相较于零值切片的82081ns提升了4.3倍性能。

  • 相关阅读:
    IGES文件在线渲染与转换方法
    人工智能AI 全栈体系(三)
    Prometheus抓取springBoot指标并grafana可视化
    Java序列化和反序列化
    LeetCode 42. Trapping Rain Water
    java计算机毕业设计在线课程教学大纲系统源码+系统+lw+数据库+调试运行
    MySQL日志系统
    【无标题】
    MySQL日志(undo log 和 redo log 实现事务的原子性/持久性/一致性)
    Go-Zero 业务开发军火库
  • 原文地址:https://blog.csdn.net/pbrlovejava/article/details/128175941