slice

slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。 数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分, 限制了它的表达能力,比如 [3]int[4]int 就是不同的类型。 而切片则非常灵活,它可以动态地扩容。切片的类型和长度无关。 数组就是一片连续的内存, slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。

数据结构 #

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  • slice 的底层数据结构中的 array 是一个指针,指向的是一个 Array
  • len 代表这个 slice 的元素个数
  • cap 表示 slice 指向的底层数组容量
对 slice 的赋值,以值作为函数参数时,只拷贝 1 个指针和 2 个 int 值。

操作 #

创建 #

  • var []T[]T{}
  • func make([]T,len,cap) []T

nil 切片和空切片 #

  • nil 切片被用在很多标准库和内置函数中,描述一个不存在的切片的时候,就需要用到 nil 切片。比如函数在发生异常的时候,返回的切片就是 nil 切片。nil 切片的指针指向 nil.
  • 空切片一般会用来表示一个空集合。比如数据库查询,一条结果也没有查到,那么就可以返回一个空切片。

扩容 #

计算策略 #

  • 若 Slice cap 大于 doublecap,则扩容后容量大小为 新 Slice 的容量(超了基准值,我就只给你需要的容量大小)
  • 若 Slice len 小于 1024 个,在扩容时,增长因子为 1(也就是 3 个变 6 个)
  • 若 Slice len 大于 1024 个,在扩容时,增长因子为 0.25(原本容量的四分之一)

内存策略 #

  • 翻新扩展:当前元素为 kindNoPointers,也就是非指针类型,将在老 Slice cap 的地址后继续申请空间用于扩容
  • 举家搬迁:重新申请一块内存地址,整体迁移并扩容

拷贝 #

slicecopy() 方法会把源切片值(即 from Slice ) 中的元素复制到目标切片(即 to Slice ) 中, 并返回被复制的元素个数,copy 的两个类型必须一致。slicecopy() 方法最终的复制结果取决于较短的那个切片, 当较短的切片复制完成,整个复制过程就全部完成了。

特性 #

slice 的 array 存储在连续内存上,因此具有以下特点:

  1. 随机访问很快,适合下标访问,缓存命中率很高;
  2. 动态扩容会涉及内存拷贝和开辟新内存,会带来 gc 压力,内存碎片化;
  3. 如果可预估使用空间,提前分配 cap 的大小是极好的;
  4. 新、老 slice 共用底层数组,对底层数组的更改都会影响到彼此;
  5. append 可以掰断新老 slice 共用底层数组的关系;

参考资料 #