Go 语言切片(Slice)
底层结构、append 扩容、三索引切片、以及共享底层数组的坑。
底层结构
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 当前长度
cap int // 容量
}len is how many elements you can access; cap is the space from the start to the end of the backing array. Understanding the two explains every "weird" slice behaviour.
创建
var a []int // nil 切片,len=0 cap=0
b := []int{1, 2, 3}
c := make([]int, 3) // len=3 cap=3,全 0
d := make([]int, 2, 5) // len=2 cap=5
e := a[0:0] // 空切片但非 nil
fmt.Println(a == nil, e == nil)
fmt.Println(len(b), len(c), cap(c), len(d), cap(d))true false
3 3 3 2 5append 与扩容
s := make([]int, 0, 2)
fmt.Println(len(s), cap(s)) // 0 2
s = append(s, 1)
fmt.Println(len(s), cap(s)) // 1 2
s = append(s, 2)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 3) // 超容量,重新分配
fmt.Println(len(s), cap(s)) // 3 4
s = append(s, 4, 5, 6, 7, 8)
fmt.Println(len(s), cap(s)) // 8 80 2
1 2
2 2
3 4
8 8Growth policy shifts slightly between versions: roughly double below 256, ~1.25× above. Growth copies every element, so pre-allocate with make([]T, 0, n) when the size is known.
共享底层数组的坑
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // len=2 cap=4,共享底层数组
b[0] = 99
fmt.Println(a) // a 也变了!
b = append(b, 100) // 容量够,直接写进 a 的位置
fmt.Println(a)[1 99 3 4 5]
[1 99 3 100 5]slices.Clone(a[1:3]) (Go 1.21+) or append([]int(nil), a[1:3]...). Most often hit when parsing packets or paging data.三索引切片
a := []int{1, 2, 3, 4, 5}
b := a[1:3:3] // 第三个数字限制 cap
fmt.Println(len(b), cap(b)) // 2 2
b = append(b, 100) // 必须重新分配,不影响 a
fmt.Println(a)
fmt.Println(b)2 2
[1 2 3 4 5]
[2 3 100]a[low:high:max] sets cap = max - low. Putting a wall in front of append is a common way for libraries to stop callers mutating internal data.
常用操作
s := []int{1, 2, 3, 4, 5}
// 删除下标 2
s = append(s[:2], s[3:]...)
fmt.Println(s)
// 在中间插入
s = append(s[:1], append([]int{99}, s[1:]...)...)
fmt.Println(s)
// 复制
dst := make([]int, len(s))
n := copy(dst, s)
fmt.Println(n, dst)
// 反转
slices.Reverse(s)
fmt.Println(s)[1 2 4 5]
[1 99 2 4 5]
5 [1 99 2 4 5]
[5 4 2 99 1]| Function | Use |
|---|---|
append | 追加 |
copy | 复制,返回复制的个数 |
slices.Clone | 独立副本 |
slices.Contains | 是否包含 |
slices.Index | 找下标 |
slices.Sort | 排序 |
slices.Reverse | 反转 |
底层结构
type slice struct {
array unsafe.Pointer // 指向底层数组
len int // 当前长度
cap int // 容量
}len 是能访问的元素个数,cap 是从起始位置到底层数组末尾的空间。理解这两个数字的差别,就能理解切片所有的「诡异」行为。
创建
var a []int // nil 切片,len=0 cap=0
b := []int{1, 2, 3}
c := make([]int, 3) // len=3 cap=3,全 0
d := make([]int, 2, 5) // len=2 cap=5
e := a[0:0] // 空切片但非 nil
fmt.Println(a == nil, e == nil)
fmt.Println(len(b), len(c), cap(c), len(d), cap(d))true false
3 3 3 2 5append 与扩容
s := make([]int, 0, 2)
fmt.Println(len(s), cap(s)) // 0 2
s = append(s, 1)
fmt.Println(len(s), cap(s)) // 1 2
s = append(s, 2)
fmt.Println(len(s), cap(s)) // 2 2
s = append(s, 3) // 超容量,重新分配
fmt.Println(len(s), cap(s)) // 3 4
s = append(s, 4, 5, 6, 7, 8)
fmt.Println(len(s), cap(s)) // 8 80 2
1 2
2 2
3 4
8 8扩容策略随版本微调,大致是:容量小于 256 时翻倍,大于 256 时按 1.25 倍增长。扩容意味着复制全部元素,所以已知大小时预分配 make([]T, 0, n) 能省掉多次复制。
共享底层数组的坑
a := []int{1, 2, 3, 4, 5}
b := a[1:3] // len=2 cap=4,共享底层数组
b[0] = 99
fmt.Println(a) // a 也变了!
b = append(b, 100) // 容量够,直接写进 a 的位置
fmt.Println(a)[1 99 3 4 5]
[1 99 3 100 5]slices.Clone(a[1:3])(Go 1.21+)或 append([]int(nil), a[1:3]...)。这个坑在解析网络包、分页处理时最容易踩。三索引切片
a := []int{1, 2, 3, 4, 5}
b := a[1:3:3] // 第三个数字限制 cap
fmt.Println(len(b), cap(b)) // 2 2
b = append(b, 100) // 必须重新分配,不影响 a
fmt.Println(a)
fmt.Println(b)2 2
[1 2 3 4 5]
[2 3 100]a[low:high:max] 的 max 决定 cap = max - low。给 append 设一道墙,是库代码里防止调用方意外改到内部数据的常用手法。
常用操作
s := []int{1, 2, 3, 4, 5}
// 删除下标 2
s = append(s[:2], s[3:]...)
fmt.Println(s)
// 在中间插入
s = append(s[:1], append([]int{99}, s[1:]...)...)
fmt.Println(s)
// 复制
dst := make([]int, len(s))
n := copy(dst, s)
fmt.Println(n, dst)
// 反转
slices.Reverse(s)
fmt.Println(s)[1 2 4 5]
[1 99 2 4 5]
5 [1 99 2 4 5]
[5 4 2 99 1]| 函数 | 用途 |
|---|---|
append | 追加 |
copy | 复制,返回复制的个数 |
slices.Clone | 独立副本 |
slices.Contains | 是否包含 |
slices.Index | 找下标 |
slices.Sort | 排序 |
slices.Reverse | 反转 |