slice
约 742 字大约 2 分钟
2024-05-19
slice 的底层结构
在 Go 中,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案
底层结构(runtime/slice.go):
type slice struct {
array unsafe.Pointer
len int
cap int
}切片增长(append)
切片有三个属性,指针(array)、长度(len) 和容量(cap)。append 时有两种场景:
- 当 append 之后的长度小于等于 cap,将会直接利用原底层数组剩余的空间。
- 当 append 后的长度大于 cap 时,则会分配一块更大的区域来容纳新的底层数组
因此,为了避免内存发生拷贝,如果能够知道最终的切片的大小,预先设置 cap 的值能够获得最好的性能。
扩容规则
Go 的扩容策略在 runtime.growslice 中实现(不同版本略有调整)
slice 扩容时,当原容量小于 256 时按 2 倍增长;当原容量不小于 256 时,采用近似 1.25 倍的递增公式逐步扩容,直到满足所需容量,若一次追加超出翻倍上限则直接扩容到所需长度。
// nextslicecap computes the next appropriate slice length.
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 {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) >> 2
// We need to check `newcap >= newLen` and whether `newcap` overflowed.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
if uint(newcap) >= uint(newLen) {
break
}
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
return newLen
}
return newcap
}扩容导致的陷阱
func f(a []int) {
// 传参时拷贝了新的切片,因此当新切片的长度发生改变时,原切片并不会发生改变。
a = append(a, 6, 7, 8, 9, 10)
// 因为发生了扩容,这个时候新切片和原切片指向的底层数组就不是同一个了。
// 因此,对新切片第 0 个元素的修改,并不会影响原切片的第 0 个元素。
a[0] = 100
}
func main() {
a := []int{1, 2, 3, 4, 5}
f(a)
fmt.Println(a) // [1 2 3 4 5]
}如果如果希望 f 函数的操作能够影响原切片呢?
- 设置返回值,将新切片返回并赋值给 main 函数中的变量 a。
- 切片也使用指针方式传参。
- 提前分配好空间,避免 slice 扩容。
数组和切片
- 在 Go 语言中,数组是一种值类型,而且不同长度的数组属于不同的类型。例如 [2]int 和 [20]int 属于不同的类型。
- 当值类型作为参数传递时,参数是该值的一个拷贝,因此更改拷贝的值并不会影响原值。
func f(a [5]int) {
a[0] = 100
}
func main() {
a := [5]int{1, 2, 3, 4, 5}
f(a)
fmt.Println(a) // [1 2 3 4 5]
}