Golang中slice深入探究
Golang 中的 slice 是一个灵活且强大的数据结构,用于处理和操作一组连续的元素。它比数组更加灵活,适用于大多数需要动态数组的场景。
slice 是对数组的抽象封装,底层数据存储在一个数组中。slice 的切片操作不会复制底层数组的数据,而是创建一个新的 slice 结构,指向相同的底层数组。
基础知识
slice 是对数组的抽象封装,底层数据存储在一个数组中。slice 的切片操作不会复制底层数组的数据,而是创建一个新的 slice 结构,指向相同的底层数组。因此,多个 slice 可以共享相同的底层数据,修改其中一个 slice 可能会影响其他共享同一底层数组的数据。
和数组的区别
长度和容量
- 数组的长度是固定的,在声明时就需要指定。
- Slice 的长度是可变的,可以动态增加或减少。
初始化和声明
- 数组在声明时必须指定长度,可以在声明时初始化元素
- Slice 可以在声明时不指定长度,只需指定元素类型。可以使用内置的
make函数来创建一个指定长度和容量的 slice。
底层实现
- 数组是一个固定长度的连续内存块,直接存储在栈或堆上。每次使用数组时都会复制整个数组。
- Slice 是对数组的一个抽象封装,包含一个指向底层数组的指针、长度和容量。多个 slice 可以共享同一个底层数组。
数据结构
1type slice struct {
2 array unsafe.Pointer
3 len int
4 cap int
5}
array:指向数组内存空间的起始位置len:切片的长度cap:切片的容量,需要满足:cap >= len
初始化
声明不初始化
1var s []int
此时切片的字面量是一个空指针nil。
make()
通过make来初始化slice的时候,有两种方式,一种是指定cap,一种是不指定cap。
不指定cap
1s = make([]int, 10)
此时len=10,切cap=10。
指定cap
1s = make([]int, 10, 20)
此时切片s的len为10,且cap为20。
在[len, cap) 之间范围内的内存空间虽然已经分配,但是直接访问会出现下标越界的panic。
但是[0, len) 之间范围内的元素是可以直接访问到的,没有初始化的位置为0值。
赋值初始化
1s := []int {1, 2, 3}
此时切片是的len=3,cap=3
源码
1// makeslice 创建一个新的切片,并分配指定的长度和容量。
2// 参数:
3// - et: *_type 指向切片元素类型的指针。
4// - len: int 指定切片的长度。
5// - cap: int 指定切片的容量。
6// 返回值:返回一个指向分配内存的指针,类型为 unsafe.Pointer。
7func makeslice(et *_type, len, cap int) unsafe.Pointer {
8 // 计算所需的内存大小 mem,即 et.size * cap。
9 // 如果乘法溢出或者所需内存大于最大可分配内存,则返回 overflow 为 true。
10 mem, overflow := math.MulUintptr(et.size, uintptr(cap))
11
12 // 检查溢出、内存超限和长度是否合法。如果满足任一条件,则处理错误。
13 if overflow || mem > maxAlloc || len < 0 || len > cap {
14 // NOTE: 当用户使用 make([]T, bignumber) 时,产生 'len out of range' 错误而不是 'cap out of range' 错误。
15 // 'cap out of range' 也是正确的,但由于容量是隐含提供的,说长度更清楚。
16 // 详情见 golang.org/issue/4085。
17
18 // 重新计算内存大小 mem,此时为 et.size * len。
19 // 如果乘法溢出或者所需内存大于最大可分配内存,或者长度为负,则抛出长度越界的错误。
20 mem, overflow := math.MulUintptr(et.size, uintptr(len))
21 if overflow || mem > maxAlloc || len < 0 {
22 panicmakeslicelen()
23 }
24 // 如果上面检查通过,但长度大于容量,则抛出容量越界的错误。
25 panicmakeslicecap()
26 }
27
28 // 分配内存,并返回指向该内存的指针。第三个参数 true 表示内存需要初始化。
29 return mallocgc(mem, et, true)
30}
截取
形如:
1s[1:2]
2s[:4]
3s[0:]
4s[:]
s[a:b]:可以看成是截取从a到b-1这个下标的元素。即:[a, b) 这样一个左闭右开的区间。
在对切片进行截取操作的时候,底层复用的都是同一块内存区域。新截取出来的切片会创建一个新的slice header
append()
append操作会再slice最后的位置新增一个元素,这里的末尾指的是len这个长度,而不是cap。
1s = append(s, 1)
扩容
扩容的主要代码位于:runtime/slice.go
1// growslice 扩展一个切片的容量,并返回新的切片。
2// 参数:
3// - et: *_type 指向切片元素类型的指针。
4// - old: slice 旧的切片。
5// - cap: int 新的容量。
6// 返回值:返回新的切片。
7func growslice(et *_type, old slice, cap int) slice {
8 // ...
9
10 // 如果新的容量小于旧的容量,抛出错误。
11 if cap < old.cap {
12 panic(errorString("growslice: cap out of range"))
13 }
14
15 // 如果元素大小为 0,返回一个新的切片,其指向 zerobase 且长度为 old.len,容量为 cap。
16 if et.size == 0 {
17 return slice{unsafe.Pointer(&zerobase), old.len, cap}
18 }
19
20 // 初始化新的容量 newcap 为旧容量。
21 newcap := old.cap
22 // 计算双倍容量 doublecap。
23 doublecap := newcap + newcap
24 if cap > doublecap {
25 // 如果预期的新容量大于双倍容量,则直接使用新容量。
26 newcap = cap
27 } else {
28 const threshold = 256
29 if old.cap < threshold {
30 // 如果旧容量小于阈值,直接使用双倍容量。
31 newcap = doublecap
32 } else {
33 // 对于大于阈值的情况,逐步增加容量,过渡到增长 1.25 倍。
34 for 0 < newcap && newcap < cap {
35 newcap += (newcap + 3*threshold) / 4
36 }
37 // 如果计算的新容量溢出,则直接使用指定的新容量。
38 if newcap <= 0 {
39 newcap = cap
40 }
41 }
42 }
43
44 var overflow bool
45 var lenmem, newlenmem, capmem uintptr
46 // 针对常见的元素大小进行优化。
47 switch {
48 case et.size == 1:
49 // 数组元素的大小为1,则新容量的大小为1 * newCap
50 lenmem = uintptr(old.len)
51 newlenmem = uintptr(cap)
52 capmem = roundupsize(uintptr(newcap))
53 overflow = uintptr(newcap) > maxAlloc
54 newcap = int(capmem)
55 case et.size == goarch.PtrSize:
56 // 数组元素为指针类型,则根据指针占用空间结合元素个数计算空间大小
57 lenmem = uintptr(old.len) * goarch.PtrSize
58 newlenmem = uintptr(cap) * goarch.PtrSize
59 capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
60 overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
61 newcap = int(capmem / goarch.PtrSize)
62 case isPowerOfTwo(et.size):
63 // 元素的大小为2的指数,则通过位运算来计算空间的大小
64 var shift uintptr
65 if goarch.PtrSize == 8 {
66 shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
67 } else {
68 shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
69 }
70 lenmem = uintptr(old.len) << shift
71 newlenmem = uintptr(cap) << shift
72 capmem = roundupsize(uintptr(newcap) << shift)
73 overflow = uintptr(newcap) > (maxAlloc >> shift)
74 newcap = int(capmem >> shift)
75 default:
76 // 元素的大小 * 元素的个数
77 lenmem = uintptr(old.len) * et.size
78 newlenmem = uintptr(cap) * et.size
79 capmem, overflow = math.MulUintptr(et.size, uintptr(newcap))
80 capmem = roundupsize(capmem)
81 newcap = int(capmem / et.size)
82 }
83
84 // 检查内存溢出情况。
85 if overflow || capmem > maxAlloc {
86 panic(errorString("growslice: cap out of range"))
87 }
88
89 var p unsafe.Pointer
90 if et.ptrdata == 0 {
91 // 分配未初始化的内存。
92 p = mallocgc(capmem, nil, false)
93 // 清除未被覆盖的部分。
94 memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
95 } else {
96 // 分配已初始化的内存。
97 p = mallocgc(capmem, et, true)
98 if lenmem > 0 && writeBarrier.enabled {
99 // 进行写屏障处理。
100 bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(old.array), lenmem-et.size+et.ptrdata)
101 }
102 }
103 // 将旧数据复制到新分配的内存中。
104 memmove(p, old.array, lenmem)
105
106 // 返回新的切片,指向新分配的内存。
107 return slice{p, old.len, newcap}
108}
删除
加入我们有个切片s定义如下:
1s := []int{1, 2, 3, 4, 5, 6}
删除首个
1s = s[1:]
删除末尾
1s = s[:len(s)-1]
删除中间
1s = append(s[:2], s[3:]...)
这样就删除了下标为2的元素。len=5,cap=6
清空
1s = s[:0]
这样操作以后len=0,cap=4
复制
- 直接使用赋值语句,只会新建一个
slice header,底层的数组还是指向着同一片内存区域。 - 使用
copy()方法则可以拷贝出一个独立于原来切片的新的切片对象。
— END —