1. 底层
-
runtime/slice.go
type slice struct { array unsafe.Pointer // 指向底层数组 len int // 切片元素数量 cap int // 底层数组容量 }
-
reflect/value.go
type SliceHeader struct { Data uintptr Len int Cap int }
2. 创建
-
根据数组创建
s := arr[0:3]
-
字面量:编译时插入创建数组的代码
s := []int{1, 2, 3}
-
make:运行时创建数组
s := make([]int, 10)
3. 测试
3.1 字面量创建切片底层
func main() {
s := []int{1, 2, 3}
fmt.Println(s)
}
查看 Plan9 汇编代码,运行:
go build -gcflags -S main.go
重点关注 s := []int{1,2,3}
对应的部分:
LEAQ type.[3]int(SB), AX #创建一个大小为3,类型为int的数组
PCDATA $1, $0
NOP
CALL runtime.newobject(SB) #新建一个结构体(slice)的值,并往里面塞3个数组
MOVQ $1, (AX)
MOVQ $2, 8(AX)
MOVQ $3, 16(AX)
3.2 make 创建切片
func main() {
s := make([]int, 3)
fmt.Println(s)
}
查看 Plan9 汇编代码,运行:
go build -gcflags -S main.go
重点关注 s := make([]int, 3)
对应的部分:
LEAQ type.int(SB), AX
MOVL $3, BX
MOVQ BX, CX
PCDATA $1, $0
CALL runtime.makeslice(SB) #直接调用 makeslice 方法
4. 访问
- 下标访问
- range 遍历
- len 查看切片长度
- cap 查看数组容量
5. 追加
s := []int{1, 2, 3}
s = append(s, 4, 5)
如果 append 后,len > cap,则需要做扩容。
6. 扩容
底层调用 growslice()
方法:
6.1 Go1.17 及之前的 growslice()
func growslice(et *_type, old slice, cap int) slice {
// 检查
...
// 确定新 cap
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
// ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
newcap = cap
} else {
// ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 1024,则 cap 直接翻倍
if old.cap < 1024 {
newcap = doublecap
} else {
// ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 1024,则每次增长 25%
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// 新建数组,复制,字节对齐
....
}
6.2 Go1.18 的 growslice()
func growslice(et *_type, old slice, cap int) slice {
// 检查
...
// 确定新 cap
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
// ① 如果新 cap 大于两倍旧 cap,则直接使用新 cap
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
// ② 如果新 cap 小于两倍旧 cap 且旧 cap 小于 256,则 cap 直接翻倍
newcap = doublecap
} else {
// ③ 如果新 cap 小于两倍旧 cap 且旧 cap 大于 256,则增加幅度逐渐从 2x 降到 1.25x
// 原始容量 扩容系数
// 256 2.0
// 512 1.63
// 1024 1.44
// 2048 1.35
// 4096 1.30
for 0 < newcap && newcap < cap {
newcap += (newcap + 3*threshold) / 4
}
...
}
}
// 新建数组,复制,字节对齐
....
}
总结
- Go1.17 及以前
- 如果 newcap 大于 2*oldcap,则直接使用 newcap
- 否则
- 如果 oldcap < 1024,则 2*oldcap
- 如果 oldcap >= 1024,则 1.25*oldcap
- Go1.18
- 如果 newcap 大于 2*oldcap,则直接使用 newcap
- 否则
- 如果 oldcap < 256,则 2*oldcap
- 如果 oldcap >= 256,则 oldcap += (oldcap + 3*256) / 4
在 Go1.18 中,优化了切片在容量较大时扩容的策略,让底层数组大小的增长更加平滑:通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从 2 到 1.25 的突变。该 commit 作者给出了几种原始容量下对应的“扩容系数”:
原始容量 | 扩容系数 |
---|---|
256 | 2.0 |
512 | 1.63 |
1024 | 1.44 |
2048 | 1.35 |
4096 | 1.30 |
PS:slice 在扩容的时候是并发不安全的,在并发访问的时候,需要加锁。