slice/切片-动态数组,golang常用的数据结构之一,相对于数组,slice可以追加元素,在容量不足时自动扩容
当前go版本:1.24
数据结构
todo:文章图片待补充
slice数据结构如下所示
1 2 3 4 5 6
| type slice struct { array unsafe.Pointer len int cap int }
|
其中
array
- 指向一片连续内存区域的第一个元素
len
- 已有元素数量
cap
- 可容纳元素总数量
初始化
slice初始化方式有三种
- 使用字面量创建新切片
- 使用关键字make创建切片
- 通过下标获取数组或切片的一部份
1 2 3 4 5 6
| var v1 = []int{1, 2, 3}
var v2 = make([]int, 10)
var v3 = v2[1:5]
|
字面量
使用ssa
包打印var v1 = []int{1, 2, 3}
,得到结果如下所示
1 2 3 4 5 6 7 8
| t0 = new [3]int (slicelit) *[3]int t1 = &t0[0:int] *int *t1 = 1:int t2 = &t0[1:int] *int *t2 = 2:int t3 = &t0[2:int] *int *t3 = 3:int t4 = slice t0[:]
|
- 根据字面量的数量创建一个数组t0
- 初始化数组元素t1/t2/t3
- 在t0基础上创建一个切片t4
src/cmd/compile/internal/walk/complit.go
slicelit
关键字
使用ssa
包打印var v2 = make([]int, 10)
,得到结果如下所示
1 2
| t0 = new [10]int (makeslice) *[10]int t1 = slice t0[:10:int] []int
|
- 创建一个数组t0
- 在t0基础上创建一个切片t1
makeslice
源代码如下,做了一些精简
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| func makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.Size_, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap { mem, overflow := math.MulUintptr(et.Size_, uintptr(len)) if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } panicmakeslicecap() }
return mallocgc(mem, et, true) }
|
使用下标
1 2
| var v2 = make([]int, 10) var v3 = v2[1:5]
|
使用ssa
包打印上述代码,得到结果如下所示
1 2 3
| t0 = new [10]int (makeslice) *[10]int t1 = slice t0[:10:int] []int t2 = slice t1[1:int:5:int] []int
|
- 使用关键字make创建一个切片t1
- 在上一步基础上,创建一个切片t2,指定下标left=1 right=5
追加和扩容
1 2
| var v1 = []int{1, 2, 3} v1 = append(v1, 4, 5, 6)
|
slice使用append
关键字实现追加操作,当数据量超过底层cap大小时,触发扩容,扩容代码精简后如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice { newcap := nextslicecap(newLen, oldCap)
var overflow bool var lenmem, newlenmem, capmem uintptr noscan := !et.Pointers() switch { default: lenmem = uintptr(oldLen) * et.Size_ newlenmem = uintptr(newLen) * et.Size_ capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap)) capmem = roundupsize(capmem, noscan) newcap = int(capmem / et.Size_) capmem = uintptr(newcap) * et.Size_ } }
func nextslicecap(newLen, oldCap int) int { newcap := oldCap doublecap := newcap + newcap if newLen > doublecap { return newLen }
const threshold = 256 if oldCap < threshold { return doublecap } for { newcap += (newcap + 3*threshold) >> 2 if uint(newcap) >= uint(newLen) { break } }
if newcap <= 0 { return newLen } return newcap }
|
在1.18版本,Keith Randall对slice的growth factor进行了调整,使其增长更为平滑,详细见提交:https://github.com/golang/go/blob/2dda92ff6f9f07eeb110ecbf0fc2d7a0ddd27f9d/src/runtime/slice.go
不同cap对应的扩容因子如下
starting cap |
growth factor |
256 |
2.0 |
512 |
1.63 |
1024 |
1.44 |
2048 |
1.35 |
4096 |
1.30 |
拷贝切片
切片的拷贝也归类于appendSlice,具体在src/cmd/compile/internal/walk/assign.go:appendSlice
。
这里的copy可能与我们预期不一样,并不是浅层或深层拷贝,是拷贝与目标dst的len长度一致的数据,与容量cap无关
1 2 3
| var v1 = []int{1, 2, 3} var v2 []int copy(v2, v1)
|
slicecopy函数具体如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| func slicecopy(toPtr unsafe.Pointer, toLen int, fromPtr unsafe.Pointer, fromLen int, width uintptr) int { if fromLen == 0 || toLen == 0 { return 0 }
n := fromLen if toLen < n { n = toLen }
if width == 0 { return n }
size := uintptr(n) * width if size == 1 { *(*byte)(toPtr) = *(*byte)(fromPtr) } else { memmove(toPtr, fromPtr, size) } return n }
|
参考文档
1. Memory Allocation Summary.md
3.2 切片
ssa
Go Slices: usage and internals
Arrays, Slices and Maps in Go