slice
slice 是数组的引用,但是本身是结构体:
1 | // runtime/slice.go |
len 和 cap
1 | s := make([]string, 1, 3) |
创建 slice:
- 创建一个长度为
cap
的数组,如果不指定cap
,则cap
等于len
;例如s := []string{"a","b","c"}
的len
和cap
都是 3; - 将数组前
len
个元素进行初始化,上例中数组第一个元素 k 初始化为空字符串; - 返回。
slice 的分割
slice 的分割不涉及复制操作:它只是新建了一个结构来放置一个不同的指针,长度和容量:
分割表达式x[1:3]
并不分配更多的数据:它只是创建了一个新的 slice 来引用相同的存储数据。
1 | s1 := []int{1, 2, 3} |
修改 s1,也会影响到 s2。
字符串的分割也同理:
append 和 copy
append
现有的元素加上要添加的元素,长度不超过 cap,则不会发生扩容行为,只会修改被引用的数组和len
:
1 | s1 := make([]int, 2, 100) |
append 添加的元素太多,当前底层的数组不够用了,就会自动扩容,会复制被引用的数组,然后切断引用关系。
copy
上面的例子中:
1 | s1 := []int{1, 2, 3} |
修改 s1,也会影响到 s2,如果想避免这种情况,需要使用copy(dst, src)
:
1 | s1 := []int{1, 2, 3} |
slice 的扩容
在对 slice 进行 append 等操作时,可能导致 slice 会自动扩容,重新分配更大的数组。go1.18 之前其扩容的策略是:
- 如果新的大小是当前大小 2 倍以上,则大小增长为新大小;
- 否则循环操作:如果当前大小小于 1024,按每次 2 倍增长,否则每次按当前大小 1/4 增长。
go1.18 之后,优化了切片扩容的策略 2,让底层数组大小的增长更加平滑:
1 | newcap := old.cap |
通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变,作者给出了几种原始容量下对应的“扩容系数”:
原始容量 | 扩容系数 |
---|---|
256 | 2.0 |
512 | 1.63 |
1024 | 1.44 |
2048 | 1.35 |
4096 | 1.30 |
什么时候用 slice?
在 go 语言中 slice 是很灵活的,大部分情况都能表现的很好,但也有特殊情况。
当程序要求 slice 的容量超大并且需要频繁的更改 slice 的内容时,就不应该用 slice,改用list更合适。