Slice详解
基础用法
-
slice定义,在
runtime/slice.go中1 2 3 4 5type slice struct { array unsafe.Pointer len int cap int } -
slice拥有指针、长度、容量这几个字段,其底层是一个数组,用指针array指代
-
切片操作
切片名[startIndex:endIndex:maxIndex],其操作的区间是[startIndex, endIndex),是左闭右开的,最多取到下标maxIndex-1处,endIndex<=maxIndex -
注意与python区分:
- python的切片第二个冒号后是步长step,
- 不支持像python切片那样的支持逆置
slice[::-1]以及slice[-1],逆置只能手动写for循环;访问最后一个元素只能slice[len(slice)-1]
-
slice的5种创建方式:
1 2 3 4 5 6var slice1 []int // nil slice slice2 := []int{1, 2, 3} slice3 := make([]int, 1, 2) // 长度为1,容量为2,长度必须小于等于容量 slice4 := *new([]int) // nil slice,注意取值运算* slice5 := slice2[1:2] slice6 := []struct{x, y int}{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} -
注意事项
- 使用make初始化slice时,必须指明长度
- 指向同一个底层数组的不同切片A和B, 若使用A修改了A和B的公共部分,那么访问B时也会受到影响.
- 此外,还需要注意两个指向同一底层数组、但起始和结束位置以及容量大小不同的切片,经过一系列扩容操作后,可能不再指向同一底层数组。
- 使用
make()创建新切片时,可以不指定容量,容量默认和长度相同。 nil slice不等于empty slice:nil slice指代的是和nil做==运算的结果是true,其len和cap都是0;empty slicec虽然len和cap为0,但是和nil做==运算是false
扩容机制
代码在runtime/slice.go/growslice(et *_type, old slice, cap int)中
先大致计算新容量newcap,然后进行内存对齐得到最终的新容量。具体为:
- 记当前容量为oldcap, 新增B个元素后为cap=oldcap + B
- 第一步计算大致新容量newcap:
- 如果
cap > 2*oldcap, 那么新容量newcap = cap - 如果
cap <= 2*oldcap,且oldcap<1024,那么新容量newcap = 2 * oldcap - 如果
cap <= 2*oldcap, 且oldcap>=1024大于1024时,先令newcap=oldcap, 循环操作:newcap=newcap*1.25, 直至newcap>=cap或长度太大导致overflow时退出循环
- 如果
- 第二步内存对齐:
-
首先,需要注意函数
growslice()中,et指的是element type,即切片中元素的类型,_type类型的et是对切片类型的抽象和描述 -
然后,根据不同的切片元素类型进行处理,大致思路是计算申请et.size * newcap大小的内存,Go内存分配器计算需要的内存大小
x=roundupsize(et.size * newcap),切片最终容量newcap = x / et.size -
计算
lenmem,newlenmem,capmem,overflow,newcap的详细如下,根据切片每个元素大小的不同进行不同的计算: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 33switch { case et.size == 1: lenmem = uintptr(old.len) newlenmem = uintptr(cap) capmem = roundupsize(uintptr(newcap)) overflow = uintptr(newcap) > maxAlloc newcap = int(capmem) case et.size == sys.PtrSize: lenmem = uintptr(old.len) * sys.PtrSize newlenmem = uintptr(cap) * sys.PtrSize capmem = roundupsize(uintptr(newcap) * sys.PtrSize) overflow = uintptr(newcap) > maxAlloc/sys.PtrSize newcap = int(capmem / sys.PtrSize) case isPowerOfTwo(et.size): var shift uintptr if sys.PtrSize == 8 { // Mask shift for better code generation. 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) } -
src/runtime/msize/roundupsize()1 2 3 4 5 6 7 8 9 10 11 12 13 14// Returns size of the memory block that mallocgc will allocate if you ask for the size. func roundupsize(size uintptr) uintptr { if size < _MaxSmallSize { if size <= smallSizeMax-8 { return uintptr(class_to_size[size_to_class8[divRoundUp(size, smallSizeDiv)]]) } else { return uintptr(class_to_size[size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]]) } } if size+_PageSize < size { return size } return alignUp(size, _PageSize) }- 注意注释:当我们请求size大小的内存块时,返回mallocgc将分配的实际内存块的大小。
- 里面的大致实现是利用提前计算好的数组,以O(1)的代价计算出结果
-
GC
slice = slice[:0], 将slice的len设置为0, 保留分配的内存slice = nil将len和cap置为0,且触发GC回收内存(如果底层数组不存在其他引用)
slice的花式用法
Go中没有cpp中的vector,也没有python中的list,官方大概觉得slice可以实现这两个容器的所有功能….
用slice实现cpp中vector的主要功能
push_back()push_front()pop_back()pop_front()insert()erase()size()capacity()reverse()copy()clear()
|
|
其他:
- slice应该不能实现
resize(), 底层数组的改变不受用户自主控制 - 此外批量初始化,例如n个1,应该需要写循环