go 切片(slice)
go切片是go中十分方便的一种包装,它本身不拥有任何数据,只是对现有的数组进行引用。但是在切片的容量(capaciy)发生改变时,切片指向的数组地址时候改变的,也就是说,在切片容量发生改变时,go内部会申请一个新的数组,然后将原数组中的数据复制到新数组中,同时切片指向新的数组。
下面我们通过代码来实际感受一下。
1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice", sSlice)
sSlice[0] = 2
fmt.Println("s:", s, ", sSlice", sSlice)
输出:
1
2
s: [1 2 3] , sSlice: [1]
s: [2 2 3] , sSlice: [2]
在这段代码中sSlice
是对数组s
的一个切片,当我们将sSlice
的第一个元素修改后,数组s
的值也相应修改。
1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice:", sSlice)
sSlice = append(sSlice, 5)
fmt.Println("s:", s, ", sSlice:", sSlice)
输出:
1
2
s: [1 2 3] , sSlice: [1]
s: [1 5 3] , sSlice: [1 5]
同理,当以append的方式往切片中加入数据时,也会导致原数组的改变,注意此时切片的容量是没有改变的:依然为数组s
的长度3
。
下面我们来编写一种会导致切片容量改变的情况。
1
2
3
4
5
s := [...]int {1,2,3}
sSlice := s[:1]
fmt.Println("s:", s, ", sSlice:", sSlice)
sSlice = append(sSlice, 5, 6, 7)
fmt.Println("s:", s, ", sSlice:", sSlice)
输出:
1
2
s: [1 2 3] , sSlice: [1]
s: [1 2 3] , sSlice: [1 5 6 7]
现在由于我们往切片中添加了3
个元素,原来切片的容量3
已经无法存储下以前切片中的1
一个元素和添加的3
个元素,共4
个元素了,所以切片会自动扩容。而当切片扩容后,此时的append
函数返回的就是一个新的切片对新的数组的引用了,而添加的值也是添加到新的切片中(实际是底层的新数组)。
切片的扩容
上面我们知道了切片在发生扩容时会只想一个新的数组,所以我们在编码的时候就需要知道切片什么时候会发生扩容,以便于我们规避由切片指向的数组改变而我们没有意识到而产生bug。
我们先看看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
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ...省略部分
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
// ...省略部分
}
- 当
cap
大于切片原容量的两倍时,切片的容量会增加为cap的值1 2 3 4
cap = old.cap + append传入元素个数 if cap != 1 and cap%2 != 0 { cap += 1 // 当 cap!=1 时,保证cap是偶数 }
- 如果
cap
在切片原容量的两倍范围内时,需要区分原数组长度是否小于1024
,如果小于,直接容量加倍,如果大于,容量以1/4
为增量增加,直至容量符合需要的大小。
原创作品,转载请注明来源 https://hebicheng.github.io