切片Slice的底层原理
go数组是值类型,赋值和函数传参操作都会复制整个数组数据。
切片是引用传递,不需要额外的内存且比使用数组更有效率。
切片的结构体由三部分组成,array是指向真实数组的指针,len代表当前切片的长度,cap是当前切片的容量。cap总是大于等于len的。指针指向切片元素对应的底层数组元素的指针,长度对应切片中元素的数量,长度不能超过容量,容量一般是从切片的开始位置到底层数据的结尾位置的长度。
slice的make会被编译器翻译成makeslice或makeslice64方法:
- 判断容量是否太大导致内存溢出
- 如果溢出或者需要的内存大于最大能分配的内存或大小<0或者大小比容量大,则panic
- 从堆中分配底层数组,返回指向数组开始位置的指针
参考博文:https://mp.weixin.qq.com/s/ngH1QN__DmU9VjX36Bz10Q
- 切片的一个特点是,被截取后的数组仍然指向原始切片的底层数据。
- 在go语言中,切片的复制其实也是值复制,但这里的值复制是对于运行时SliceHeader结构的复制,底层指针仍然指向相同的底层数据地址,因此可以理解为数据进行了引用传递。这一特性使得即便切片中有大量的数据,在复制时的成本也比较小,与数组显著不同。
- 在编译时使用NewSlice函数新建一个切片类型,并需要传递切片元素的类型。可以看出,切片元素的类型elem是在编译期间确定的。
- 字面量初始化:形如[]int{1,2,3},会创建一个array数组存储在静态区中,并在堆中创建一个新的切片,在程序启动时将静态区的数据复制到堆区,这样可以加快切片的初始化过程。
- make初始化:编译时对与字面量的重要优化是判断变量应该被分配到栈还是应该逃逸到堆区。如果make函数初始化了一个太大的切片,则该切片会逃逸到堆中。如果分配了一个比较小的切片,则会再接在栈中分配。
- 如果没有逃逸,那么切片运行时最终会被分配到栈中。如果发生了逃逸,那么运行时调用makesliceXX函数会将切片分配在堆中。当切片的长度和容量小于int类型的最大值时,会调用makeslice函数,反之调用makeslice64函数创建切片。
append底层
- append函数可以添加新元素到切片的末尾,可以接受可变长度的元素,并且可以自动扩容。
- 删除切片的第一个元和最后一个元素都非常容易,如果要删除切片中间的某一段或某一个元素,可以借助切片的截取特性,通过截取删除元素前后的切片数组,再使用append函数拼接,这种处理方式比较优雅,并且效率很高,因为它不会申请额外的内存空间。
append会被翻译成两个步骤:
-
slice的大小加上append的元素的个数与slice的容量作比较
-
如果slice的大小加上1<容量,则直接修改底层数组就可以了,如果大于的话就会跳转到runtime.growslice方法:
func growslice(et *_type, old slice, cap int) slice {}
- cap是指slice的大小加上append元素个数后的值,表示大小至少要这么多
- 计算把原来的容量进行翻倍的值
- 如果需要的大于原来的两倍容量,则新容量选择需要的容量,意味着append后所有容量都被占用,大小等于容量,如果这个slice频繁append则会一直扩容,降低性能。
- 如果两倍容量足够存放数据且元素个数小于1024,则新容量就设置为双倍
- 如果原来的容量个数超过1024个,则按照25%增长,直到足够。这种做法不至于浪费内存,但如果这个slice频繁append,则会一直扩容,降低性能。
- 计算得到append前数据占用空间大小,append后数据占用空间大小,新容量占用空间大小,修正新的容量
- 申请新容量空间
- 将原slice中的数据copy到新空间
- 最后把新的slice返回
- copy与append两种深拷贝方式,copy性能更好,建议使用copy。