go源码之Slice

go源码之Slice

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
}

参考资料

非懂不可的 Slice(二) (qq.com)

(28 条消息) Go 源码分析——Slice 篇_go slice 源码_卑微的程序猿的博客-CSDN 博客

[(28 条消息) 「Golang」Slice 源码讲解_golang slice 源码__ Echo_的博客-CSDN 博客](