1 前言
切片 slice 是 golang
中的一个非常经典的数据结构,它的定位可以类比于其它编程语言中的数组。本篇文章介绍的内容会分为源码实现以及使用 slice 会遇到的问题,走读的源码为 go v1.22.5。
2 源码实现
2.1 数据结构
type slice struct {
	array unsafe.Pointer // 指向起点的地址
	len   int // 切片的长度
	cap   int // 切片的容量
}
- array:指向了内存地址空间的起点。因为- slice是存放在连续的内存空间上面,当我们需要获取- slice中的值时,可以通过索引- index,在起点的基础上快速进行地址偏移,从而定位到目标元素。
- len:切片的长度,指的是- slice逻辑意义上实际存放的长度。
- cap:切片的容量,指的是- slice实际物意义上分配了多少个元素的内存空间。
 
2.2 初始化
func makeslice(et *_type, len, cap int) unsafe.Pointer {
 	// 通过每个元素占用的内存和 cap 计算出所需要的内存总大小
	mem, overflow := math.MulUintptr(et.Size_, uintptr(cap))
	// 倘若容量超限,len 取负值或者 len 超过 cap,直接 panic
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		mem, overflow := math.MulUintptr(et.Size_, uintptr(len))
		// 倘若长度超限,len 取负值或者 len 超过 cap,直接 panic
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}
	return mallocgc(mem, et, true)
}
上述方法的核心步骤是:
- 通过 math.MulUintptr方法,根据每个元素的大小以及切片的cap,计算出初始化切片所需的内存空间大小。
- 如果内存空间超出限制,则直接 panic。
- 调用 runtime/malloc.go中的mallocgc方法进行内存的分配。
2.3 引用传递
首先需要捋清楚引用传递和值传递的两个区别。
 
引用传递就是将实例对象的地址信息传递到方法内部去,这样在方法里面就可以通过地址追溯到实例所在的位置,因此执行的一些修改操作就会直接影响到原实例。
 
值传递,指的就是对实例对象进行一次拷贝,得到一个副本,然后将这个副本传递到方法中去,这样在方法里面所修改的只是对这个副本进行操作,不会影响到原实例对象。
在本次聊的 slice 就是属于引用传递:
func changeSlice(s []int) {
	s[0] = 0
}
func main() {
	s := []int{1, 2, 3}
	fmt.Println(s)
	changeSlice(s)
	fmt.Println(s)
}
代码运行的结果为:
[1 2 3]
[0 2 3]
可以看到,通过方法里面进行修改切片元素,原实例里面的数据也发生了变化。
产生这个结果的原因就在于 slice 的传递是引用传递,并不是值传递,因为每一个切片都对应一个数据结构 slice header
,其中存储了三个字段:
- array:切片内存空间的起始地址 。
- len:切片长度。
- cap:切片容量。
在每次在方法之间传递切片的时候,会对 slice header 进行一次值拷贝,然后将 slice header
传递到方法中去,但是这个  slice header 副本中的 array 和原 slice 指向同一片内存空间,因此在方法内部进行修改的时候,也会对原切片进行修改。
2.4 切片扩容
发生切片扩容的情况是:当前 slice 的 len 和 cap 的值相同时,下一次 append 操作就会发生一次切片的扩容。
func main() {
	s := []int{1, 2, 3}
	fmt.Printf("before len %d, cap %d\n", len(s), cap(s))
	s = append(s, 4)
	fmt.Printf("after len %d, cap %d\n", len(s), cap(s))
}
代码运行的结果为:
before len 3, cap 3
after len 4, cap 6
通过运行结果可以看出,在 len 和 cap 的值相同时,发生了扩容操作。
切片的扩容流程位于 runtime/slice.go 文件下面的 growslice 和 nextslicecap 方法中,其中核心步骤如下:
- 如果新的长度小于 0,直接 panic。
- 如果切片元素的大小为 0 (元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回。
- 如果预期的容量超过原容量的两倍,直接设置新容量为预期容量。
- 如果原容量的大小小于 256 字节,则直接采用原容量的两倍作为新容量。
- 如果原容量的大小已经大于了 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止。
- 通过 mallocgc流程,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小。
- 调用 mallocgc,对新切片进行内存初始化。
- 调用 memmove方法,将老切片中的内容拷贝到新切片中。
- 返回扩容后的新切片。
 
func nextslicecap(newLen, oldCap int) int {
	// 初始化新容量为旧容量
	newcap := oldCap
	// 计算两倍旧容量
	doublecap := newcap + newcap
	// 如果新长度大于两倍旧容量,直接返回新长度
	if newLen > doublecap {
		return newLen
	}
  
	// 定义一个阈值,用于区分小切片和大切片
	const threshold = 256
	// 如果老容量小于阈值,直接返回两倍的老容量
	if oldCap < threshold {
		return doublecap
	}
  
	// 对于大切片,使用平滑增长的公式
	for {
		// 平滑增长的公式:新容量增加 (新容量 + 3 * 阈值) 的 1/4
		newcap += (newcap + 3*threshold) >> 2
		if uint(newcap) >= uint(newLen) {
			break
		}
	}
	// 如果新容量计算溢出,设置新容量为新长度
	if newcap <= 0 {
		return newLen
	}
	return newcap
}
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 := nextslicecap(newLen, oldCap)
	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// 判断元素类型是否包含指针
	noscan := et.PtrBytes == 0
	// 根据元素类型的大小,计算旧长度、新长度和容量的内存大小
	switch {
	// 如果元素大小为 1 字节
	case et.Size_ == 1:
		// 计算旧长度的内存大小
		lenmem = uintptr(oldLen)
		// 计算新长度的内存大小
		newlenmem = uintptr(newLen)
		capmem = roundupsize(uintptr(newcap), noscan)
		// 计算新长度的内存大小
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	// 如果元素大小为指针大小
	case et.Size_ == goarch.PtrSize:
		// 计算旧长度的内存大小
		lenmem = uintptr(oldLen) * goarch.PtrSize
		// 计算新长度的内存大小
		newlenmem = uintptr(newLen) * goarch.PtrSize
		// 计算新容量的内存大小,并向上取整
		capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
		// 检查新容量是否超过最大分配限制
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		// 更新新容量
		newcap = int(capmem / goarch.PtrSize)
	// 如果元素大小是 2 的幂
	case isPowerOfTwo(et.Size_):
		// 计算元素大小的位移量
		var shift uintptr
		if goarch.PtrSize == 8 {
			// 计算 64 位系统下的位移量
			shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
		} else {
			// 计算 32 位系统下的位移量
			shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
		}
		// 计算旧长度的内存大小
		lenmem = uintptr(oldLen) << shift
		// 计算新长度的内存大小
		newlenmem = uintptr(newLen) << shift
		// 计算新容量的内存大小,并向上取整
		capmem = roundupsize(uintptr(newcap)<<shift, noscan)
		// 检查新容量是否超过最大分配限制
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		// 更新新容量
		newcap = int(capmem >> shift)
		// 重新计算新容量的内存大小
		capmem = uintptr(newcap) << shift
	default:
		// 旧长度的内存大小
		lenmem = uintptr(oldLen) * et.Size_
		// 新长度的内存大小
		newlenmem = uintptr(newLen) * et.Size_
		// 新容量的内存大小
		capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
		// 根据 mallocgc 重新计算容量的内存大小
		capmem = roundupsize(capmem, noscan)
		// 新的容量大小
		newcap = int(capmem / et.Size_)
		capmem = uintptr(newcap) * et.Size_
	}
	// 检查内存空间是否超限
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: len out of range"))
	}
	var p unsafe.Pointer
	// 判断是否包含指针
	if et.PtrBytes == 0 {
		// 分配 capmem 容量的大小,但是不初始化内存,不处理写屏障
		p = mallocgc(capmem, nil, false)
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		p = mallocgc(capmem, et, true)
		if lenmem > 0 && writeBarrier.enabled {
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
		}
	}
	// 将旧数组中的数据复制到新数组中
	memmove(p, oldPtr, lenmem)
	// 返回新的切片,包含新的底层数组指针、新长度和新容量
	return slice{p, newLen, newcap}
}
2.5 切片拷贝
slice 的拷贝分为简单拷贝和完整拷贝两种情况。
如果想要实现简单拷贝,只需要对切片的字面量进行赋值传递即可,这样就相当于创建了一个新的 slice header 的实例,但是其中的指针 array 、容量 cap 和长度 len 仍然和老的 slice header 实例相同。
func main() {
	s1 := []int{0, 1, 2, 3, 4}
	s2 := s1
	fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
address of s1: 0x1400001c180, address of s2: 0x1400001c180
使用切片的截取操作也是属于简单拷贝,通过下面代码可以看出,s1 和 s2 会使用同一个内存空间,只是地址起点位置偏移了一个元素的长度 s1 和 s2 的地址,刚好差 8 个 byte 。
func main() {
	s1 := []int{0, 1, 2, 3, 4}
	s2 := s1[1:]
	fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
address of s1: 0x14000128030, address of s2: 0x14000128038
有两种情况会创建完整的复制,也就是创建出一个和 slice 容量大小相等的内存区域,并将原 slice 中的元素一一拷贝到新空间中。
方法一:调用系统 copy 方法,可以看出 s1 和 s2 的地址是相互独立的:
func main() {
	s1 := []int{0, 1, 2, 3, 4}
	s2 := make([]int, len(s1))
	copy(s2, s1)
	fmt.Println(s1, s2)
	fmt.Printf("address of s1: %p, address of s2: %p", s1, s2)
}
显示运行结果
[0 1 2 3 4] [0 1 2 3 4]
address of s1: 0x1400001c180, address of s2: 0x1400001c1b0
方法二:一开始 s1 赋值给 s2 是浅拷贝,s2 的 len 和 cap 分别为 4、4,当执行 append 操作之后,s2 发生了扩容操作,导致将原来切片中的数据赋值到了新的切片中,s1 和 s2 的地址空间不一样了,修改 s2 也不会导致 s1 中的值发生变化。
func main() {
	s1 := []int{0, 1, 2, 3, 4}
	s2 := s1[1:]
	fmt.Printf("address of s1: %p, address of s2: %p\n", s1, s2)
	s2 = append(s2, 5)
	fmt.Printf("address of s1: %p, address of s2: %p\n", s1, s2)
	fmt.Println(s1, s2)
	s2[0] = 10
	fmt.Println(s1, s2)
}
显示运行结果
address of s1: 0x14000116030, address of s2: 0x14000116038
address of s1: 0x14000116030, address of s2: 0x14000120040
[0 1 2 3 4] [1 2 3 4 5]
[0 1 2 3 4] [10 2 3 4 5]
3 问题
3.1 问题1
- 初始化切片 s 长度和容量均为 10。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 10)
	s = append(s, 10)
	fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 20
3.2 问题2
- 初始化切片 s 长度为 0,容量为 10。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 0, 10)
	s = append(s, 10)
	fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [10], len of s: 1, cap of s: 10
3.3 问题3
- 初始化切片 s 长度为 10,容量为 11。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 10, 11)
	s = append(s, 10)
	fmt.Printf("s: %v, len of s: %d, cap of s: %d", s, len(s), cap(s))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0 10], len of s: 11, cap of s: 11
3.4 问题4
- 初始化切片 s 长度为 10,容量为 12
- 截取切片 s index = 8 往后的内容赋给 s1
请问 s1 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:]
	fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d", s1, len(s1), cap(s1))
}
显示运行结果
s1: [0 0], len of s1: 2, cap of s1: 4
3.5 问题5
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index 为 [8,9) 范围内的元素赋给切片 s1。
请问 s1 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:9]
	fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d", s1, len(s1), cap(s1))
}
显示运行结果
s1: [0], len of s1: 1, cap of s1: 4
3.6 问题6
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 修改 s1[0] 的值。
请问这个修改是否会影响到 s? 此时,s 的内容是什么?
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:]
	s1[0] = -1
	fmt.Printf("s: %v", s)
}
显示运行结果
s: [0 0 0 0 0 0 0 0 -1 0]
3.7 问题7
- 初始化切片 s 长度为 10,容量为 12
请问,访问 s[10] 是否会越界?
func main() {
	s := make([]int, 10, 12)
	v := s[10]
	fmt.Printf("v: %d", v)
}
显示运行结果
panic: runtime error: index out of range [10] with length 10
3.8 问题8
- 初始化切片 s 长度为 10,容量为 12。
- 截取 s 中 index = 8 后面的内容赋给 s1。
- 在 s1 的基础上追加 []int{10,11,12} 3 个元素。
请问,经过上述操作时候,访问 s[10] 是否会越界?
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:]
	s1 = append(s1, []int{10, 11, 12}...)
	v := s[10]
	fmt.Printf("v: %d", v)
}
显示运行结果
panic: runtime error: index out of range [10] with length 10
3.9 问题9
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 在方法 changeSlice 中,对 s1[0] 进行修改。
求问,经过上述操作之后,s 的内容是什么?
func changeSlice(s1 []int) {
	s1[0] = -1
}
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:]
	changeSlice(s1)
	fmt.Printf("s: %v", s)
}
显示运行结果
s: [0 0 0 0 0 0 0 0 -1 0]
3.10 问题10
- 初始化切片 s 长度为 10,容量为 12。
- 截取切片 s index = 8 往后的内容赋给 s1。
- 在方法 changeSlice 中,对 s1 进行 apend 追加操作。
请问,经过上述操作后,s 以及 s1 的内容、长度和容量分别是什么?
func changeSlice(s1 []int) {
	s1 = append(s1, 10)
}
func main() {
	s := make([]int, 10, 12)
	s1 := s[8:]
	changeSlice(s1)
	fmt.Printf("s: %v, len of s: %d, cap of s: %d\n", s, len(s), cap(s))
	fmt.Printf("s1: %v, len of s1: %d, cap of s1: %d\n", s1, len(s1), cap(s1))
}
显示运行结果
s: [0 0 0 0 0 0 0 0 0 0], len of s: 10, cap of s: 12
s1: [0 0], len of s1: 2, cap of s1: 4
3.11 问题11
- 初始化切片 s,内容为 []int{0,1,2,3,4}。
- 截取 s 中 index = 2 前面的内容(不含s[2]),并在此基础上追加 index = 3 后面的内容。
请问,经过上述操作后,s 的内容、长度和内容分别是什么?此时访问 s[4] 是否会越界?
func main() {
	s := []int{0, 1, 2, 3, 4}
	s = append(s[:2], s[3:]...)
	fmt.Printf("s: %v, len: %d, cap: %d\n", s, len(s), cap(s))
	v := s[4]
	fmt.Printf("v: %v\n", v)
}
显示运行结果
s: [0 1 3 4], len: 4, cap: 5
panic: runtime error: index out of range [4] with length 4
3.12 问题12
- 初始化切片 s 长度和容量均为 512。
- 在 s 的基础上追加 append 一个元素。
请问经过上述操作后,切片s 的内容、长度以及容量分别是什么?
func main() {
	s := make([]int, 512)
	s = append(s, 1)
	fmt.Printf("len of s: %d, cap of s: %d", len(s), cap(s))
}
显示运行结果
len of s: 513, cap of s: 848
4 总结
- slice是一个长度可变的连续数据序列,在实现上基于一个- slice header组成,其中包含的字段包括:指向内存空间地址起点的指针- array、一个表示了存储数据长度的- len和分配空间长度的- cap。
- 由于 slice在传递过程中,本质上传递的是slice header实例中的内存地址array,因此属于引用传递。
- slice在扩容时,遵循如下机制:- 如果扩容时预期的新容量超过原容量的两倍,直接取预期的新容量。
- 如果原容量小于 256,直接取原容量的两倍作为新容量。
- 如果原容量大于等于 256,在原容量 n的基础上循环执行n += (n+3*256)/4的操作,直到n大于等于预期新容量,并取n作为新容量。
 
- 当执行 copy系统调用或者副本的slice发生了扩容的操作时,会将副本的slice拷贝到新的切片中。
5 参考
- 小徐先生的编程世界
- Golang 源码 1.22.5