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 存储在连续内存上,因此具有以下特点:
- 随机访问很快,适合下标访问,缓存命中率很高;
- 动态扩容会涉及内存拷贝和开辟新内存,会带来 gc 压力,内存碎片化;
- 如果可预估使用空间,提前分配 cap 的大小是极好的;
- 新、老 slice 共用底层数组,对底层数组的更改都会影响到彼此;
- append 可以掰断新老 slice 共用底层数组的关系;