slice切片的实现原理
组是值类型 每次传参都用数组 每次数组都要被复制一遍 会消耗掉大量的内存
函数传参用数组的指针 原数组的指针指向更改了 函数里面的指针指向也会跟着更改
用切片类型传数组参数 既可以达到节约内存的目的 合理处理好共享内存的问题
结论 把第一个大数组传递参数 消耗很多内存 采用切片传参可以避免问题 切片是引用传递 所以他们不需要使用额外的内存并且比
数组更有效率
当然 也有例外 切片底层数组 可能会在堆上分配内存 小数组在栈上堵塞拷贝的消耗 未必比make大
切片的数据结构
切片本身并不是动态数组或者数组指针 它内部实现的数据结构通过指针引用底层数组 设定相关属性根据读写操作限定在指定的区域内
切片本身知识一个只读对象 其工作机制类似数组指针的一种封装
切片是对数组一个连续片段的引用 所以切片是一个引用类型 这个片段可以是整个数组 或者是由起始和终止索引的一些项的自己
终止索引表示的项不包括在切片内 切片提供了一个与指针数组的动态窗口
切片的结构体 由3部分组成 Pointer是指向一个数组的指针 len代表当前切片的长度 cap是当前切片的容量 cap总是大于len
Slice的结构体定义如下
type slice struct{array unsafe.Pointerlen intcap int
}
切片的结构体由三部分构成 其中的Pointer是指向数组的指针 len是代表当前切片的长度 cap是当前切片的容量 cap总是大于等于len的
从sliice中得到一块内存地址
s := make([byte,200]) ptr := unsafe.Pointer(&s[0])
从Go的内存滴哦之中构造一个slice
var ptr unsafe.Pointer
var s1 = {adder uintptrlen intcap int
}{pte,length, length}
s := *(*[]byte)(unsafe.Pointer(&s1))
构造一个虚拟的结构体 把slice的数据结构拼出来
在Go的反射中就存在一个与之对应的结构体SliceHeader,我们可以用它来构造一个slice
varo []byte sliceHeader := (*reflect.SliceHeader)((unsafe.Pointer(*&o))) skiceHeader.Cap = length sliceHeader.Len = length sliceHeader.Data = uinptr(ptr)
-
创建切片
make函数允许在运行期间指定数组长度 绕开了数组类型必须在编译期常量的限制
创建切片用两种形式 make创建切片空切片
func makeslice(et *_type, len, cap int) slice {// 根据切片的额数据类型,获得切片的最大容量maxElements := maxSliceCap(et.size)// 比较切片的长度,长度值域应该在[0,maxElements]之间if len < 0 || uintptr(len) > maxElements {panic(errorString("makeslice: len out of range"))}// 比较切片的容量,容量值域应该在[len,maxElements]之间if cap < len || uintptr(cap) > maxElements {panic(errorString("makeslice: cap out of range"))}// 根据切片的容量申请内存p := mallocgc(et.size*uintptr(cap), et, true)// 返回申请好内存的切片的首地址return slice{p, len, cap} }假设用make函数创建了一个len = 4,cap = 6的切片 内存空间申请了6各int类型的内存大小 ,由于 len = 4
所以后i面的2个暂时访问不到 但是容量还是在的 这时候数组里面的每个变量都是0
var slice []int 创建了一个空切片
nil切片被用在很多标准库的内置函数中,描述一个不存的切片的时候,就需要用到nil切片
比如说函数在发生异常的时候与,返回的切片就是nilqiepian nil切片的指针指向nil
-
空切片和nil切片的区别在于,空切片指向的地址不是nil,指向的是一个内存地址,但是它没有分配任何
-
的内存空间 即底层元素包含0个元素
-
不管是使用nil切片还是空切片,对其调用的内置函数append,len,cap的效果都是一样的
切片扩容
-
func growslice(et *_type, old slice, cap int) slice {if raceenabled {callerpc := getcallerpc(unsafe.Pointer(&et))racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))}if msanenabled {msanread(old.array, uintptr(old.len*int(et.size)))}
if et.size == 0 {// 如果新要扩容的容量比原来的容量还要小,这代表要缩容了,那么可以直接报panic了。if cap < old.cap {panic(errorString("growslice: cap out of range"))}
// 如果当前切片的大小为0,还调用了扩容方法,那么就新生成一个新的容量的切片返回。return slice{unsafe.Pointer(&zerobase), old.len, cap}}
// 这里就是扩容的策略newcap := old.capdoublecap := newcap + newcapif cap > doublecap {newcap = cap} else {if old.len < 1024 {newcap = doublecap} else {for newcap < cap {newcap += newcap / 4}}}
// 计算新的切片的容量,长度。var lenmem, newlenmem, capmem uintptrconst ptrSize = unsafe.Sizeof((*byte)(nil))switch et.size {case 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))newcap = int(capmem)case ptrSize:lenmem = uintptr(old.len) * ptrSizenewlenmem = uintptr(cap) * ptrSizecapmem = roundupsize(uintptr(newcap) * ptrSize)newcap = int(capmem / ptrSize)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem = roundupsize(uintptr(newcap) * et.size)newcap = int(capmem / et.size)}
// 判断非法的值,保证容量是在增加,并且容量不超过最大容量if cap < old.cap || uintptr(newcap) > maxSliceCap(et.size) {panic(errorString("growslice: cap out of range"))}
var p unsafe.Pointerif et.kind&kindNoPointers != 0 {// 在老的切片后面继续扩充容量p = mallocgc(capmem, nil, false)// 将 lenmem 这个多个 bytes 从 old.array地址 拷贝到 p 的地址处memmove(p, old.array, lenmem)// 先将 P 地址加上新的容量得到新切片容量的地址,然后将新切片容量地址后面的 capmem-newlenmem 个 bytes 这块内存初始化。为之后继续 append() 操作腾出空间。memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)} else {// 重新申请新的数组给新切片// 重新申请 capmen 这个大的内存地址,并且初始化为0值p = mallocgc(capmem, et, true)if !writeBarrier.enabled {// 如果还不能打开写锁,那么只能把 lenmem 大小的 bytes 字节从 old.array 拷贝到 p 的地址处memmove(p, old.array, lenmem)} else {// 循环拷贝老的切片的值for i := uintptr(0); i < lenmem; i += et.size {typedmemmove(et, add(p, i), add(old.array, i))}}}// 返回最终新切片,容量更新为最新扩容之后的容量return slice{p, old.len, newcap}
}
-
扩容celve
如果切片的容量小于1024个元素,于是扩容的时候就翻倍的增加容量
一旦元素的个数超过1024个元素,那么增长因子就变成1.25,即每次增加原来容量的四分之一
扩容扩大的容量都是针对原来的容量而言的,而不是震度一原来数组的长度而言的
-
扩容结果扩容以后没有新建一个新的数组,扩容前后的数组都是同一个
-
导致新的切片修改了一个值,也影响到了老的值,并且append()操作也改变了原来数组里面的值
原始数组的容量可以扩容 执行append()的操作以后,会直接在原数组上操作,所以这种情况下
扩容以后的数组还是指向原来的数组
扩容策略 中之所以产生了新的切片,是因为原来的数组的容量已经道了最大值
要想扩容 Go会先开一片内存区域 把原来的值拷贝过来,然后在执行append()操作
切片拷贝
func slicecopy(to, fm slice, width uintptr) int {// 如果源切片或者目标切片有一个长度为0,那么就不需要拷贝,直接 return if fm.len == 0 || to.len == 0 {return 0}// n 记录下源切片或者目标切片较短的那一个的长度n := fm.lenif to.len < n {n = to.len}// 如果入参 width = 0,也不需要拷贝了,返回较短的切片的长度if width == 0 {return n}// 如果开启了竞争检测if raceenabled {callerpc := getcallerpc(unsafe.Pointer(&to))pc := funcPC(slicecopy)racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)}// 如果开启了 The memory sanitizer (msan)if msanenabled {msanwrite(to.array, uintptr(n*int(width)))msanread(fm.array, uintptr(n*int(width)))} size := uintptr(n) * widthif size == 1 { // TODO: is this still worth it with new memmove impl?// 如果只有一个元素,那么指针直接转换即可*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer} else {// 如果不止一个元素,那么就把 size 个 bytes 从 fm.array 地址开始,拷贝到 to.array 地址之后memmove(to.array, fm.array, size)}return n }
-
slicecopy方法会把源切片的值中的元素赋值道目标切片中,并返回被复制的元素个数,copy的两个类型必须一致,slicecopy方法最终复制的结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就完成了
用range的方式遍历一个切片,拿到的Value其实是切片里面的值拷贝
所以 Value是值拷贝的,并非引用传递所以直接修改value是达不到更改源切片的目的 需要通过&slice[index]获取真实的地址
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
