Golang 切片slice源码走读

切片slice是golang中非常经典的数据结构,其定位可以类比其他语言中的动态数组。 切片中的元素存放在一块内存地址连续的区域,使用索引可以快速检索到指定位置的元素;切片长度和容量是可变的,在使用过程中可以根据需要进行扩容。
本文对应的Golang版本是1.21.9

数据结构

// src/runtime/slice.go
type slice struct {
    array unsafe.Pointer
    len int
    cap int
}

array 指针指向底层数组, len表示切片长度,cap表示底层数组容量。

初始化

声明和初始化主要通过 1) 变量声明 2) 字面量 3) 使用内置函数make() 4) 从切片和数组中切取

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 {
		mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocgc(mem, et, true)
}
  1. 调用 math.MulUintptr 的方法,结合每个元素的大小以及切片的容量,计算出初始化切片所需要的内存空间大小。
  2. 如果容量超限,len 取负值或者 len 超过 cap,直接 panic
  3. 调用位于 runtime/malloc.go 文件中的 mallocgc 方法,为切片进行内存空间的分配

元素追加

a := []int{1,2,3,4,5}
b := a[1:4]
b = append(b, 10) // 此时元素a[4]将由5变成0

此时需要借助扩展表达式 (完整切片表达式)
a := []int{1,2,3,4,5}
b := a[1:4:4]
b = append(b, 10)

切片扩容

func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	oldLen := newLen - num
	...

	if newLen < 0 {
		panic(errorString("growslice: len out of range"))
	}
	if et.Size_ == 0 {
		// 倘若元素大小为 0,则无需分配空间直接返回
		return slice{unsafe.Pointer(&zerobase), newLen, newLen}
	}

	newcap := oldCap  // 计算扩容后数组的容量
	doublecap := newcap + newcap   // 取原容量两倍的容量数值
	if newLen > doublecap {
		// 倘若新的容量大于原容量的两倍,直接取新容量作为数组扩容后的容量
		newcap = newLen
	} else {
		const threshold = 256
		// 倘若原容量小于 256,则扩容后新容量为原容量的两倍
		if oldCap < threshold {
			newcap = doublecap
		} else {
			// 在原容量的基础上,对原容量 * 5/4 
            // 循环执行上述操作,直到扩容后的容量已经大于等于预期的新容量为止
			for 0 < newcap && newcap < newLen {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// 倘若数值越界了,则取预期的新容量 cap 封顶
			if newcap <= 0 {
				newcap = newLen
			}
		}
	}

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// 基于容量,确定新数组容器所需要的内存空间大小 capmem
	switch {
	// 倘若数组元素的大小为 1,则新容量大小为 1 * newcap.
    // 同时会针对 span class 进行取整
	case et.Size_ == 1:
		...
	// 倘若数组元素为指针类型,则根据指针占用空间结合元素个数计算空间大小
    // 并会针对 span class 进行取整
	case et.Size_ == goarch.PtrSize:
		...
	// 倘若元素大小为 2 的指数,则直接通过位运算进行空间大小的计算  
	case isPowerOfTwo(et.Size_):
		...
	/ 兜底分支:根据元素大小乘以元素个数
    // 再针对 span class 进行取整  
	default:
		capmem = roundupsize(uintptr(newcap))
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	}
	// 进行实际的切片初始化操作
    var p unsafe.Pointer
    // 非指针类型
    if et.ptrdata == 0 {
        p = mallocgc(capmem, nil, false)
        // ...
    } else {
        // 指针类型
        p = mallocgc(capmem, et, true)
        // ...
    }
    // 将切片的内容拷贝到扩容后的位置 p 
    memmove(p, old.array, lenmem)
	return slice{p, newLen, newcap}
}

切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下:
切片的扩容流程的核心步骤

func Test_slice(t *testing.T){
    s := make([]int,512)  
    s = append(s,1)
    t.Logf("len of s: %d, cap of s: %d",len(s),cap(s))
}
len: 513, cap: 848
  1. 切片扩容操作:由于切片 s 原有容量为 512,已经超过了阈值 256,因此对其进行扩容操作会采用的计算方式为 512 * (512 + 3*256)/4 = 832
  2. 然后结合分配内存的 mallocgc 流程,将 8byte * 832 = 6656 byte 补齐到6784 byte 的这一档次。扩容后实际的新容量为 cap = 6784/8 = 848。
// class  bytes/obj  bytes/span  objects  tail waste  max waste  min align
//     1          8        8192     1024           0     87.50%          8
//     2         16        8192      512           0     43.75%         16
//     3         24        8192      341           8     29.24%          
// ...
//    48       6528       32768        5         128      6.23%        128
//    49       6784       40960        6         256      4.36%        128 

元素删除

// 删除尾部元素
func Test_slice(t *testing.T){
    s := []int{0,1,2,3,4}
    // [0,1,2,3]
    s = s[0:len(s)-1]
}

// 删除中间元素
func Test_slice(t *testing.T){
    s := []int{0,1,2,3,4}
    // 删除 index = 2 的元素
    s = append(s[:2],s[3:]...)
    // s: [0,1,3,4], len: 4, cap: 5
    t.Logf("s: %v, len: %d, cap: %d", s, len(s), cap(s))
}

// 删除所有元素
func Test_slice(t *testing.T){
    s := []int{0,1,2,3,4}
    s = s[:0]
    // s: [], len: 0, cap: 5
    t.Logf("s: %v, len: %d, cap: %d", s, len(s), cap(s))
}

切片拷贝


// s 和 s1 的地址是一致的.

func Test_slice(t *testing.T) {
    s := []int{0, 1, 2, 3, 4}
    s1 := s
    t.Logf("address of s: %p, address of s1: %p", s, s1)
}

// s 和 s1 的地址是相互独立的
func Test_slice(t *testing.T) {
    s := []int{0, 1, 2, 3, 4}
    s1 := make([]int, len(s))
    copy(s1, s)
    t.Logf("s: %v, s1: %v", s, s1)
    t.Logf("address of s: %p, address of s1: %p", s, s1)
}

总结

  1. 关于slice的切割赋值,如果cap比较大,仍然会导致高内存消耗,有内存泄漏的风险。此时,应该使用copy(src, dst)函数进行拷贝。
  2. Slice并非并发安全的数据结构,使用时需要注意并发安全的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值