Go 底层数据结构 |
您所在的位置:网站首页 › slice类型 › Go 底层数据结构 |
Go 底层数据结构(一)- Slice
当前 Golang 版本 1.18.1 基础概念: Slice(切片)与 Array(数组)的区别: Array 是一个长度不可改变的数据结构;Slice 是一个支持扩容的数据结构 在作为参数传递时,数组传递的是拷贝值,而切片传递的是指针。 重要知识点 slice 结构具有三个字段,分别表示:指向数组内存地址指针,容量,长度; 截取操作不会重新创建数组,而是复用原切片数组。内存地址指针根据截取起始点偏移;注意可能造成的内存溢出。同时,考虑截取切片在操作数据时对原数组是否影响;(是否发生扩容) 切片扩容规则发生改变。阈值改为 256,低于阈值为 2x 扩容,高于阈值需查表; 提前规划容量可以减少扩容产生的开销 Slice数据结构 type slice struct { array unsafe.Pointer // 指向数组内存地址的指针 len int // 已有元素长度 cap int // 切片总容量 } 复制代码 Slice初始化go/slice.go at master · golang/go (github.com) // makeslice 初始化 slice func makeslice(et *_type, len, cap int) unsafe.Pointer { // 计算 类型大小 与 容量 的乘积,用以判断切片是否会超过总体内存大小 mem, overflow := math.MulUintptr(et.size, uintptr(cap)) // 如果超过 || 使用内存超过最大可分配内存空间 || 长度为非负数 || 长度大于容量 if overflow || mem > maxAlloc || len < 0 || len > cap { // 提示:这里进行再判断,抛出一个 'len out of range' 错误,而非 'cap out of range' 错误 // 是因为 cap 是隐式提供的,而 len 相较于 cap 而言更为清晰。 // 此时会计算 类型大小 与 长度 的乘积,更精细的判断 mem, overflow := math.MulUintptr(et.size, uintptr(len)) // 如果还是超过,那么抛出 'makeslice: len out of range' if overflow || mem > maxAlloc || len < 0 { panicmakeslicelen() } // 如果长度没问题,那么则抛出 makeslice: cap out of range panicmakeslicecap() } // 如果容量没问题,那么则分配 mem 的内存空间 return mallocgc(mem, et, true) } 复制代码 切片截取我们在实际应用中,很多时候会对切片进行截取操作。例如: x := []int{2, 3, 5, 7, 11} y := x[1:3] m := x[1:] // 语法糖 n := x[:3] // 语法糖 复制代码在进行截取操作时,我们不会额外创建新的数组来传递数据,而是两者共用同一个数组 array,内存地址根据截取起始长度进行偏移。 如上图所示: 切片 x 指向了数组array为 [5]int,假定其内存地址为 t。此时对切片 x 进行截取操作得到切片 y,切片 y 中array的值应为t+8,其中+8为 int 所占字长。 综上,我们可以不需要过多关注切片截取产生的开销。 并非完全不用担心切片截取所带来的问题。在某些特定情况下,截取可能会带来内存泄露的问题。 例如,原切片 x 被 gc 正常回收,而截取切片 y 因为某些原因没有正常 gc 时,底层的数组数据就一直被截取切片引用,也不会正常被 gc 回收掉,从而造成内存泄露的情况。 源码: // makeslicecopy 申请 tolen 个元素大小的内存,然后从 from 指针中拷贝 fromlen 个元素到新内存 func makeslicecopy(et *_type, tolen int, fromlen int, from unsafe.Pointer) unsafe.Pointer { // 定义 var tomem, copymem uintptr // 如果申请元素大小 > 拷贝大小 if uintptr(tolen) > uintptr(fromlen) { var overflow bool // 重新判断新容量是否会造成内存溢出,同时计算所需内存值 tomem, overflow = math.MulUintptr(et.size, uintptr(tolen)) if overflow || tomem > maxAlloc || tolen < 0 { panicmakeslicelen() } // 如果不溢出,计算旧切片使用内存大小 copymem = et.size * uintptr(fromlen) } else { // 若老的切片容量大于等于新切片容量,且老的切片是可用的,那么新的切片内存空间也不会溢出。 // 因为他们具有相同的元素宽度(element width) // 直接计算内存大小并赋值 tomem = et.size * uintptr(tolen) copymem = tomem } // 定义一个新切片为指针类型 var to unsafe.Pointer // 判断是否是指针类型 if et.ptrdata == 0 { // 如果不是指针类型,直接分配内存地址 to = mallocgc(tomem, nil, false) if copymem < tomem { memclrNoHeapPointers(add(to, copymem), tomem-copymem) } } else { // 提示:不能分配零值内存,因为 gc 回收会扫描未分配的内存并回收 to = mallocgc(tomem, et, true) if copymem > 0 && writeBarrier.enabled { // Only shade the pointers in old.array since we know the destination slice to // only contains nil pointers because it has been cleared during alloc. // 因为我们能够确认新创建的切片只包含会被回收的空指针,所以我们只能使用旧的切片指针值赋给新切片 bulkBarrierPreWriteSrcOnly(uintptr(to), uintptr(from), copymem) } } // -race 参数 if raceenabled { callerpc := getcallerpc() pc := abi.FuncPCABIInternal(makeslicecopy) racereadrangepc(from, copymem, callerpc, pc) } if msanenabled { msanread(from, copymem) } if asanenabled { asanread(from, copymem) } // 复制从 from 到 to 长度 memmove(to, from, copymem) return to } 复制代码 切片扩容规则: 如果当前长度小于 256,每次按照 2倍增长 如果当前长度大于等于 256,根据切片类型大小有不同判定规则,近似为 1.25x + 0.75 * threshold,可以通过 sizeclasses.go 查表获取P.S. 这里是1.18 版本下新的判定阈值,在 1.18 之前阈值均为 1024,扩容规则为 2x / 1.25x 1.17 Release 版本 go/slice.go at release-branch.go1.17 · golang/go (github.com) 1.18 Release 版本 go/slice.go at release-branch.go1.18 · golang/go (github.com) 由于指针指向的数据结构是 数组,所以是其长度是不可改变的。在扩容时,是重新创建一个扩容后的长度的数组,同时将老的数据拷贝到新的数组中,并将array 指向新创建的数组。 故,扩容是一个比较耗费资源的过程。如果能预估切片的容量,我们最好为其指定容量,以避免频繁扩容带来的开销。 例如: x := make([]int, 100) // 预创建一个长度为 100 的切片 y := make([]int, 0, 100) // 预创建一个容量为 100,长度为 0 的切片 复制代码参考资料: go/slice.go at master · golang/go (github.com) slice · 《深入解析Go 》 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |