切片的底层定义
切片是一种引用类型,它包含三个属性:指针,长度和容量。源码定义如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
- array (unsafe.Pointer): 这是一个指针,指向切片所引用的数组的开始位置。
unsafe.Pointer
是 Go 中的一个特殊类型,它允许进行任意类型的指针操作。 - len (int): 表示切片的长度,即切片中元素的数量。
- cap (int): 表示切片的容量,即从原始数组的开始位置到数组结束的元素数量。它总是大于或等于切片的长度。
切片的初始化
- 直接初始化
s1 := []int{1, 2, 3} // 初始化切片
- var
var s2 []int
// 使用var声明:只分配了切片结构,不会分配数组
// array = nil, len = 0, cap = 0
- make
s3 := make([]int, 3, 5) // 长度为3,容量为5的切片
// 使用make声明:不仅分配切片结构 也分配数组
// 此处声明的切片,cap=5, len=3 分配容纳5个int的内容,并初始化为对应类型的默认值 [0, 0, 0, 0, 0]
// 添加元素,会从第4个位置开始
- new
s4 := new([]int)
// 类似var的声明,同样只分配切片结构,不分配底层的数组 new的返回值,就是s4的起始地址
// 因为没有底层数组的分配,所以 (*s4)[0]=10086 这样的操作是不允许的
// append会给它分配数组 *s4=append(*s4,10086)
切片的常用方法
- 获取长度和容量:
s := []int{1, 2, 3} // 初始化切片
len(s) // 返回切片的长度
cap(s) // 返回切片的容量
- 切片的切片:
sub := s[0:2] // 获取s的第0和第1个元素 [0,1]
- 添加元素: 切片没有直接的“添加”方法,但常用
append
函数向切片添加元素。
s = append(s, 4) // 在s的末尾添加4 [1,2,3,4]
- 合并两个切片:
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
combined := append(s1, s2...) // combined: [1,2,3,4,5,6]
- 删除元素: 切片没有直接的“删除”方法,但可以使用切片操作来删除元素。
s1 := []int{1, 2, 3, 4, 5}
// 删除索引为1的元素
s = append(s[:1], s[2:]...) // [1,3,4,5]
- 复制切片: 使用
copy
函数复制切片。
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // 将src的内容复制到dst
- 遍历切片: 使用
for
循环遍历切片。
for i, v := range s {
fmt.Println(i, v)
}
切片的扩容策略
GO1.18之前
go1.17 扩容调用的是
growslice
函数,以下是计算新容量部分的源码
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ...
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{p, old.len, newcap}
}
- 扩容策略:
- 如果期望容量大于当前容量的两倍,直接使用期望容量
- 否则
- 如果当前切片的长度小于 1024 则将容量翻倍
- 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量
GO1.18
// src/runtime/slice.go
func growslice(et *_type, old slice, cap int) slice {
// ...
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{p, old.len, newcap}
}
与旧版本的区别:
- 扩容阈值的调整,旧版本为1024 新版默认256
- 增长因子的调整
- 旧版:newcap += newcap / 4 => 按照当前容量的1/4增长,直到新容量大于期望容量
- 新版:newcap += (newcap + 3*threshold) / 4 => 如果
threshold
与newcap
相近,那么增长接近 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([]int, 3) fmt.Println(len(ints), cap(ints)) // len = 3 cap = 3 ints = append(ints, 1) // 扩容后的容量 = 扩容策略的预估容量 预估容量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([]byte, 10) fmt.Println(len(s), cap(s)) // len = 10 cap = 10 s = append(s, 1) // 扩容后的容量 != 扩容策略的预估容量 因为预估容量20占用20字节(byte占用1字节),所以匹配到的内存为24 fmt.Println(len(s), cap(s)) // len = 11 cap = 24 }
-
当预估容量已经内存对齐时,则直接使用预估容量 当预估容量内存未对齐时,会根据分配的内存来确定当前容量,即分配最靠近预估容量大小的内存
...