摘要
从两个示例看slice的底层结构和扩容机制
示例 1
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针
len int // 长度
cap int // 容量
}
func main() {
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // array 指针指向slice中的 2
s2 := s1[2:6:7] // array 指针指向slice中的 4 [4, 5, 6, 7]
fmt.Printf("s1 %v len: %d cap %d\n", s1, len(s1), cap(s1)) // [2 3 4] 3 8
fmt.Printf("s2 %v len: %d cap %d\n", s2, len(s2), cap(s2)) // [4 5 6 7] 4 5
s2 = append(s2, 100)
fmt.Printf("s2 len: %d cap %d\n", len(s2), cap(s2)) // 5 5
s2 = append(s2, 200) // 扩容之后的修改不影响slice
fmt.Printf("s2 len: %d cap %d\n", len(s2), cap(s2)) // 6 10
s1[2] = 20
fmt.Println(s1) // [2 3 20]
fmt.Println(s2) // [4 5 6 7 100 200]
fmt.Println(slice) // [0 1 2 3 20 5 6 7 100 9]
}
注意:
- 在切片引用的底层数组中从切片的第一个元素到数组最后一个元素的长度就是切片的容量
- 从大slice中切取部分元素到一个新的slice时可以使用copy做深拷贝,避免大slice内存得不到释放
- 关注append扩容时是否会发生内存拷贝,避免丢失修改
示例 2
为什么有 len = 5,cap = 6 呢?
func main() {
s := []int{1, 2}
s = append(s, 4, 5, 6)
fmt.Printf("len=%d, cap=%d", len(s), cap(s)) // 5, 6
}
从 go 1.20.1源码 src/runtime/slice.go 看扩容机制
- 根据扩容策略计算 newcap = newLen = 6
- 调用 mallocgc 前需要做内存对齐,通过 roundupsize() 函数计算得到 capmem = 48,所以 newcap = 48/8 = 6
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
...
newcap := oldCap
doublecap := newcap + newcap
if newLen > doublecap { // 5 > 4
newcap = newLen
} else {
...
}
...
switch {
...
case et.size == goarch.PtrSize:
lenmem = uintptr(oldLen) * goarch.PtrSize
newlenmem = uintptr(newLen) * goarch.PtrSize
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize) // 48
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
newcap = int(capmem / goarch.PtrSize) // 48/8 = 6
}
...
if et.ptrdata == 0 {
p = mallocgc(capmem, nil, false)
...
} else {
}
}