Golang slice扩容机制详解 | 青训营

142 阅读2分钟

Slice

Slice依托数组实现,底层数组对用户屏蔽,在底层数组容量不足时可以实现自动重分配并生成新的Slice。接下来按照实际使用场景分别介绍其实现机制。

Slice数据结构

源码包中src/runtime/slice.go:slice定义了Slice的数据结构:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

从数据结构看Slice很清晰, array指针指向底层数组,len表示切片长度,cap表示底层数组容量。

Slice 扩容

使用append向Slice追加元素时,如果Slice空间不足,将会触发Slice扩容,扩容实际上重新一配一块更大的内存,将原Slice数据拷贝进新Slice,然后返回新Slice,扩容后再将数据追加进去。

golang slice (切片) 扩容机制详解(1.18版本后)

slice源码定义

type slice struct {
	array unsafe.Pointer   //指向底层数组的指针
	len   int   //切片长度
	cap   int   //切片容量
}

growslice()方法:用于 slice 的扩容

func growslice(et *_type, old slice, cap int) slice {
   //  ......
	newcap := old.cap
	doublecap := newcap + newcap    //双倍扩容(原容量的两倍)
	if cap > doublecap {   //如果所需容量大于 两倍扩容,则直接扩容到所需容量
		newcap = cap
	} else {
		const threshold = 256   //这里设置了一个 阈值 -- 256
		if old.cap < threshold {		//如果旧容量 小于 256,则两倍扩容
			newcap = doublecap   
		} else {
	    // 检查 0 < newcap 以检测溢出并防止无限循环。
			for 0 < newcap && newcap < cap {   //如果新容量 > 0  并且 原容量 小于 所需容量
			
               // 从小片的增长2x过渡到大片的增长1.25x。这个公式给出了两者之间的平滑过渡。
				newcap += (newcap + 3*threshold) / 4
                //新容量是 = 1.25 原容量 + 3/4 阈值 (192)
                
              //当newcap计算溢出时,将newcap设置为请求的上限。
			if newcap <= 0 {   // 如果发生了溢出,将新容量设置为请求的容量大小
				newcap = cap
			}
		}
	}
}

具体情况如下:

  1. 如果请求容量 大于 两倍现有容量 ,则新容量 直接为请求容量

  2. 否则(请求容量 小于等于 两倍现有容量)

​ 如果 现有容量 小于 256 ,则新容量是原来的两倍

​ 否则:新容量 = 1.25 原容量 + 3/4 阈值 (192) “这个公式给出了从1.25倍增长 过渡到2 倍增长,两者之间的平滑过渡。” 在此情况下,如果发生了溢出,将新容量设置为请求的容量大小 slice.png

扩容容量的选择遵循以下规则:

在 1.18 版本前,切片扩容,在容量小于1024时,以2倍大小扩容。超过1024后,以1.25倍扩容。

在1.18版本后,切片扩容,在容量小于256时,以2倍大小扩容。超过256后,以(1.25倍+192)扩容。

使用append()向Slice添加一个元素的实现步骤如下:

  1. 假如Slice容量够用,则将新元素追加进去,Slice.len++,返回原Slice
  2. 原Slice容量不够,则将Slice先扩容,扩容后得到新Slice,再将新元素追加进新Slice,Slice.len++,返回新的Slice。