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