go 源码之 Slice
https://blog.csdn.net/qq_37005831/article/details/111375593)
一、总结
-
slice 是引用类型,底层是一个指向指针的数组,支持动态扩容
-
底层数据结构(三个字段)
-
array:指向所引用的数组指针(
unsafe.Pointer
可以表示任何可寻址的值的指针) -
len:长度,当前引用切片的元素个数
-
cap:容量,当前引用切片的容量(底层数组的元素总数)
-
-
底层扩容机制:当前容量小于 1024(256),则 2 倍扩容,反之则为 1.25 倍(2~1.25 之间平滑过渡)
-
扩容创建新的切片,将旧切片数据迁移到新切片,清除旧切片
-
数组与切片的区别:切片是引用类型,数组是值类型,切片可以动态扩容,底层也是一个数组
二、源码
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/a
return 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.len
if 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) * width
if 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 地址之后,拷贝个数为size
memmove(to.array, fm.array, size)
}
return n
}
参考资料
(28 条消息) Go 源码分析——Slice 篇_go slice 源码_卑微的程序猿的博客-CSDN 博客
[(28 条消息) 「Golang」Slice 源码讲解_golang slice 源码__ Echo_的博客-CSDN 博客](