切片
切片是一个动态数组的抽象,以指针引用底层数组,限定读写区域。
// runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
- 引用类型,未初始化为
nil - 基于数组,初始化值或
make函数创建 len元素数量,cap返回容量- 不支持
==,!=操作
切片截取
截取时常见创建 slice 的方法,可以从数组或者 slice 中直接截取,需要制定起止索引位置。
值得注意的时,截取的新老 slice 相互影响的前提时共用底层数组,如果因为 append 操作使得
新 slice 底层数组扩容列,移动到新的位置,那么二者就不会相互影响了。
截取操作:data[low,high,max], max>=high>=low
例如:
old := []int{0,1,2,3,4,5,6,7,8,9}
new1 := old[2:5] // new1 从索引2闭区间到索引5开区间即真正索引4位置,长度为3,容量到数组末尾即8
// new1 == [2,3,4,x,x,x,x,x] // len=3 cap=8 x:表示空
new2 := new1[2:6:7] // 从new1中2-6截取,容量到索引7,即cap=7-2=5
// new2 == [4,5,6,7,x] 因为容量还是小于old 所以还是在底层数据操作
new2 = append(new2, 100) // new2 的 len == cap == 5 长度达到容量了
new2 = append(new2, 200) // new2 需要扩容了
new1[2] = 20 // 只会更改new1 和 old slice,new2 已经不与他们共用底层数组了
fmt.Println(old) // [0,1,2,3,20,5,6,7,100,9]
fmt.Println(new1) // [2,3,20]
fmt.Println(new2) // [4,5,6,7,100,200]
Go 语⾔是如何实现切⽚扩容的?
- Go1.18之前切片的扩容是以容量1024为临界点
- 当旧容量 < 1024个元素,扩容变成2倍;
- 当旧容量 > 1024个元素,那么会进入一个循环,每次增加25%直到大于期望容量。
- 然而这个扩容机制已经被Go 1.18弃用了,官方说新的扩容机制能更平滑地过渡。就是控制让小的切片容量增长速度快一点,减少内存分配次数,而让大切片容量增长率小一点,更好地节省内存。
- Go 1.17切片扩容时会进行内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍。
- 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
- 当原 slice 容量 < 1024 的时候,新 slice 容量变成原来的2倍;
- 当原 slice 容量 > 1024,进入一个循环,每次容量变成原来的1.25倍,直到大于期望容量。
- Go1.18不再以1024为临界点,而是设定了一个值为256的
threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;- 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
- 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
- 当原 slice 容量 > threshold,进入一个循环,每次容量增加(旧容量+3*threshold)/4。
- 切片的扩容都是调用 growSlice 方法;
Go1.18 版本func growslice(et *_type, old slice, cap int) slice { // 新容量默认为原有容量 newcap := old.cap doublecap := newcap + newcap // 如果新容量大于旧容量的两倍,则直接按照新容量大小申请 if cap > doublecap { newcap = cap } else { // 如果原有长度小于1024,则新容量是旧容量的2倍 if old.len < 1024 { newcap = doublecap } else { // 按照原有容量的 1/4 增加,直到满足新容量的需要 for 0 < newcap && newcap < cap { newcap += newcap / 4 } // 检查容量是否溢出。 if newcap <= 0 { newcap = cap } } } }func growslice(et *_type, old slice, cap int) slice { ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { // 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) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } ... } - 切片在扩容时会进行内存对齐,这个和内存分配策略有关,进行内存对齐后切片扩容的容量要大于等于旧的切片容量的2倍或者1.25倍,当原切片容量小于1024的时候,新切片容量按照原来的2倍进行扩容,原slice容量超过1024的时候,新slice容量变成原来的1.25倍。
总结:尽量对切片设置初始容量值,以避免 append 调用 growslice,因为新的切片容量比旧的大,会开辟新的地址,拷贝数据,降低性能。