目录
- Go 源码之切片 Slice
- 一、总结
- 二、源码
- (一)数据结构
- (二)创建Slice
- (三)append-扩容-growslice
- (四)切片深拷贝
Go 源码之切片 Slice
go源码之Slice - Jxy 博客
一、总结
-
slice是引用类型,底层是一个指向指针的数组,支持动态扩容
-
底层数据结构(三个字段)
-
array:指向所引用的数组指针(
unsafe.Pointer
可以表示任何可寻址的值的指针) -
len:长度,当前引用切片的元素个数
-
cap:容量,当前引用切片的容量(底层数组的元素总数)
-
-
底层扩容机制:当前容量小于1024(256),则 2 倍扩容,反之则为 1.25 倍(2~1.25之间平滑过渡)
-
扩容创建新的切片,将旧切片数据迁移到新切片,清除旧切片
-
数组与切片的区别:切片是引用类型,数组是值类型,切片可以动态扩容,底层也是一个数组
二、源码
G O R O O T GOROOT GOROOT\src\runtime\slice.go
(一)数据结构
type slice struct {array unsafe.Pointer // 指向所引用的数组指针(`unsafe.Pointer` 可以表示任何可寻址的值的指针)len int // 长度,当前引用切片的元素个数cap int // 容量,cap 一定是大于或等于 len 的。否则会导致 panic
}
(二)创建Slice
makeslice64函数只是做了些校验,其内部也是调用makeslice,
makeslice函数主要是通过len、cap计算slice所需内存大小,然后调用mallocgc进行内存的分配。
// 该函数传入需要初始化的切片的类型,长度以及容量,返回的指针会通过调用方组建成一个完成的slice结构体
func makeslice(et *_type, len, cap int) unsafe.Pointer {// 调用MulUintptr函数:获取创建该切片需要的内存,以及是否溢出mem, overflow := math.MulUintptr(et.size, uintptr(cap))// 如果溢出 | 超过能够分配的最大内存(2^32 - 1) | 非法输入, 报错并返回if overflow || mem > maxAlloc || len < 0 || len > cap {// 下面的注释:// 在创建一个长度非常大的切片时,如果超出了底层数组的容量,通常会发生“cap out of range”错误。// 但是go会优先返回 len 溢出的错误mem, overflow := math.MulUintptr(et.size, uintptr(len))if overflow || mem > maxAlloc || len < 0 {// panic 错误:len溢出panicmakeslicelen()}// panic 错误:容量溢出panicmakeslicecap()}// 调用mallocgc函数分配一块连续内存并返回该内存的首地址return mallocgc(mem, et, true)
}
//MulUintptr返回a*b以及乘法是否溢出。
//在受支持的平台上,这是编译器降低的内在值。
func MulUintptr(a, b uintptr) (uintptr, bool) {if a|b < 1<<(4*goarch.PtrSize) || a == 0 {return a * b, false}overflow := b > MaxUintptr/areturn a * b, overflow
}
(三)append-扩容-growslice
append操作其实是调用了runtime/slice.go中的growslice函数
- 重新申请内存,之后将数据赋值过来
- 当原切片cap<1024 时,<新cap> = 2 * <老cap>
- 当原切片cap>1024 时,<新cap> = 1.25*<老cap>
- 之后进行内存对齐,内存对齐相关可参考【Golang】详解内存对齐
1.18 最新版本的扩容机制:
- 小于 256 则 newcap=2 x oldcap
- 大于 256,则扩容机制 在 2 ~ 1.25 倍 之间,newcap += (newcap + 3*threshold) / 4
(四)切片深拷贝
当我们使用copy时,底层调用的源码函数为makeslicecopy;
这个函数最终就是调用 runtime.memmove 来实现 slice 的 copy 的
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()pc := funcPC(slicecopy)racewriterangepc(to.array, uintptr(n*int(width)), callerpc, pc)racereadrangepc(fm.array, uintptr(n*int(width)), callerpc, pc)}if msanenabled {msanwrite(to.array, uintptr(n*int(width)))msanread(fm.array, uintptr(n*int(width)))}size := uintptr(n) * widthif size == 1 { // common case worth about 2x to do here// TODO: is this still worth it with new memmove impl?//如果只有一个元素,那么直接进行地址转换*(*byte)(to.array) = *(*byte)(fm.array) // known to be a byte pointer} else {//如果不止一个元素,那么就从 fm.array 地址开始,拷贝到 to.array 地址之后,拷贝个数为sizememmove(to.array, fm.array, size)}return n}
有劳各位看官 点赞、关注➕收藏
,你们的支持是我最大的动力!!!
接下来会不断更新 golang
的一些底层源码及个人开发经验
(个人见解)!!!
同时也欢迎大家在评论区提问、分享您的经验和见解!!!