切片的底层定义

切片是一种引用类型,它包含三个属性:指针长度容量。源码定义如下:

type slice struct {  
   array unsafe.Pointer  
   len   int  
   cap   int  
}
  1. array (unsafe.Pointer): 这是一个指针,指向切片所引用的数组的开始位置。unsafe.Pointer 是 Go 中的一个特殊类型,它允许进行任意类型的指针操作。
  2. len (int): 表示切片的长度,即切片中元素的数量。
  3. cap (int): 表示切片的容量,即从原始数组的开始位置到数组结束的元素数量。它总是大于或等于切片的长度。

切片的初始化

  • 直接初始化
s1 := []int{123}  // 初始化切片
  • var
var s2 []int  
// 使用var声明:只分配了切片结构,不会分配数组  
// array = nil, len = 0, cap = 0
  • make
s3 := make([]int35)  // 长度为3,容量为5的切片  
// 使用make声明:不仅分配切片结构 也分配数组  
// 此处声明的切片,cap=5, len=3 分配容纳5个int的内容,并初始化为对应类型的默认值 [0, 0, 0, 0, 0]  
// 添加元素,会从第4个位置开始

0a2eb4d519699a2e833c9f5842e059c0.JPG

  • new
s4 := new([]int)  
// 类似var的声明,同样只分配切片结构,不分配底层的数组 new的返回值,就是s4的起始地址  
// 因为没有底层数组的分配,所以 (*s4)[0]=10086 这样的操作是不允许的  
// append会给它分配数组 *s4=append(*s4,10086)

切片的常用方法

  • 获取长度和容量:
s := []int{123}  // 初始化切片  
len(s)  // 返回切片的长度  
cap(s)  // 返回切片的容量
  • 切片的切片:
sub := s[0:2]  // 获取s的第0和第1个元素 [0,1]
  • 添加元素: 切片没有直接的“添加”方法,但常用 append 函数向切片添加元素。
s = append(s4)  // 在s的末尾添加4 [1,2,3,4]
  • 合并两个切片:
s1 := []int{123}  
s2 := []int{456}  
combined := append(s1s2...)  // combined: [1,2,3,4,5,6]
  • 删除元素: 切片没有直接的“删除”方法,但可以使用切片操作来删除元素。
s1 := []int{12345}  
// 删除索引为1的元素  
s = append(s[:1], s[2:]...// [1,3,4,5]
  • 复制切片: 使用 copy 函数复制切片。
src := []int{123}  
dst := make([]int, len(src))  
copy(dstsrc)  // 将src的内容复制到dst
  • 遍历切片: 使用 for 循环遍历切片。
for iv := range s {  
   fmt.Println(iv)  
}

切片的扩容策略

GO1.18之前

go1.17 扩容调用的是 growslice 函数,以下是计算新容量部分的源码

// src/runtime/slice.go   
func growslice(et *_typeold slicecap intslice {  
   // ...  
  
   newcap := old.cap  
   doublecap := newcap + newcap  
   if cap > doublecap {  
	   newcap = cap  
else {  
	   if old.cap < 1024 {  
		   newcap = doublecap  
else {  
		   // Check 0 < newcap to detect overflow  
		   // and prevent an infinite loop.  
		   for 0 < newcap && newcap < cap {  
			   newcap += newcap / 4  
		  }  
		   // Set newcap to the requested cap when  
		   // the newcap calculation overflowed.  
		   if newcap <= 0 {  
			   newcap = cap  
		  }  
	  }  
  }  
  
   // ...  
  
   return slice{pold.lennewcap}  
}
  • 扩容策略:
    • 如果期望容量大于当前容量的两倍,直接使用期望容量
  • 否则
    • 如果当前切片的长度小于 1024 则将容量翻倍
    • 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量

GO1.18

// src/runtime/slice.go  
  
func growslice(et *_typeold slicecap intslice {  
   // ...  
  
   newcap := old.cap  
   doublecap := newcap + newcap  
   if cap > doublecap {  
	   newcap = cap  
else {  
	   const threshold = 256  
	   if old.cap < threshold {  
		   newcap = doublecap  
else {  
		   // Check 0 < newcap to detect overflow  
		   // and prevent an infinite loop.  
		   for 0 < newcap && newcap < cap {  
			   // 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  
		  }  
		   // Set newcap to the requested cap when  
		   // the newcap calculation overflowed.  
		   if newcap <= 0 {  
			   newcap = cap  
		  }  
	  }  
  }  
  
   // ...  
  
   return slice{pold.lennewcap}  
}

与旧版本的区别:

  • 扩容阈值的调整,旧版本为1024 新版默认256
  • 增长因子的调整
    • 旧版:newcap += newcap / 4 => 按照当前容量的1/4增长,直到新容量大于期望容量
    • 新版:newcap += (newcap + 3*threshold) / 4 => 如果 thresholdnewcap 相近,那么增长接近 100%;如果它们相差很大,那么增长将主要由 threshold 决定。
  • 公式变化:newcap = 5_newcap/4 + 3_threshold/4

    如果 threshold 近似等于 newcap,则增长接近 200%(也就是原始容量的两倍);如果 threshold 很小,接近 0,那么增长接近 125%。所以,如源码注释所写,增长的区间在 125% 到 200% 之间。这是和旧版固定增长25%不同的地方

新版本扩容策略:

  • 如果期望容量大于当前容量的两倍
    • 直接使用期望容量
  • 否则
    • 如果当前切片的长度小于 256 则将容量翻倍
    • 如果当前切片的长度大于等于 256 就会每次按照(newcap + 3*threshold) / 4的标准增加容量,直到新容量大于期望容量

      增长的区间在 125% 到 200% 之间

内存对齐

简单说,如果你请求一个不是8字节倍数的内存块,go中的内存管理模块 会将其调整到最近的8字节倍数,这样分配的内存会是8字节对齐的,从而使内存访问更加高效。

  • 简单示例:
    • 示例1

        package main  
        // go version go1.21.0 darwin/arm64  
        import (  
        	"fmt"  
        )  
      
        func main() {  
        	ints := make([]int3)  
        	fmt.Println(len(ints), cap(ints)) // len = 3 cap = 3  
        	ints = append(ints1)  
        	// 扩容后的容量 = 扩容策略的预估容量 预估容量6,占用字节 = 6 * 8 = 48. 本身已经内存对齐,所以直接匹配48字节的内存  
        	fmt.Println(len(ints), cap(ints)) // len = 4 cap = 6  
        }
      
    • 示例2

      声明一个byte的切片,长度和容量都为10,当添加一个元素之后,长度变为11 按照上述的扩容策略,此时的新容量应该是翻倍的策略,也就是cap=20 但实际测试为24,是因为内存分配时,会把最靠近20的内存块(8,16,24,32,40,48,56…),即24分配给s

        package main  
        // go version go1.21.0 darwin/arm64  
        import (  
        "fmt"  
        )  
      
        func main() {  
        s := make([]byte10)  
        fmt.Println(len(s), cap(s)) // len = 10 cap = 10  
        s = append(s1)  
        // 扩容后的容量 != 扩容策略的预估容量 因为预估容量20占用20字节(byte占用1字节),所以匹配到的内存为24  
        fmt.Println(len(s), cap(s)) // len = 11 cap = 24  
        }
      

当预估容量已经内存对齐时,则直接使用预估容量 当预估容量内存未对齐时,会根据分配的内存来确定当前容量,即分配最靠近预估容量大小的内存