Go 切片(Slice)
1. 什么是切片
切片是对底层数组的一个动态视图,包含三个要素:
| 字段 | 类型 | 说明 |
|---|---|---|
| 指针 | unsafe.Pointer | 指向底层数组中切片的起始位置 |
| 长度 | int | 切片中元素个数,len(s) |
| 容量 | int | 从起始位置到底层数组末尾的元素个数,cap(s) |
切片是引用类型,零值为
nil。nil切片的len和cap都是 0。
2. 创建切片
2.1 字面量
s := []int{1, 2, 3}
2.2 从数组或已有切片截取
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // [20, 30, 40], len=3, cap=4
截取规则:左闭右开 [low:high],容量 = 数组长度 - low。
2.3 make 函数
s := make([]int, 5) // len=5, cap=5
s := make([]int, 3, 10) // len=3, cap=10
2.4 空切片 vs nil 切片
var s1 []int // nil 切片
s2 := []int{} // 空切片(非 nil)
s3 := make([]int, 0) // 空切片(非 nil)
// 三者 len 和 cap 都为 0
// s1 == nil → true, s2 == nil → false, s3 == nil → false
// 但三者在 json 序列化时都输出 []
3. 访问与修改
s := []int{10, 20, 30, 40, 50}
s[0] // 10,读取
s[2] = 99 // 修改
// 遍历
for i, v := range s {
fmt.Println(i, v)
}
越界访问会触发
panic,运行时会检查索引是否在[0, len)范围内。
4. 追加元素 — append
s := []int{1, 2, 3}
s = append(s, 4) // [1, 2, 3, 4]
s = append(s, 5, 6, 7) // [1, 2, 3, 4, 5, 6, 7]
// 合并两个切片
s2 := []int{8, 9}
s = append(s, s2...) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
扩容机制
当 len(s) + 新元素个数 > cap(s) 时触发扩容:
| Go 版本 | 扩容策略 |
|---|---|
| < 1.21 | 新容量 = 旧容量 * 2(小切片)或 约 1.25 倍(大切片,>256) |
| >= 1.21 | 使用更平滑的增长曲线,内存上限翻倍直到 1GB,之后每次增长约 1.25 倍 |
关键点:
- 扩容后会分配新的底层数组,原数组不再共享
- 扩容后旧切片变量不变,必须用
s = append(s, ...)接收返回值 append不一定会扩容 — 如果容量足够,直接复用底层数组
s := make([]int, 3, 5) // [0, 0, 0], len=3, cap=5
s = append(s, 1) // 不扩容,cap 仍为 5
5. 复制切片 — copy
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // dst = [1, 2, 3], n = 3
- 复制的元素个数 =
min(len(dst), len(src)) dst和src的底层数组完全独立,互不影响- 深拷贝的惯用写法:
newSlice := make([]int, len(oldSlice))
copy(newSlice, oldSlice)
6. 删除元素
Go 没有内置 delete 函数删除切片元素,需要用 append + 切片表达式组合:
删除索引 i 处的元素(不保序)
s := []int{1, 2, 3, 4, 5}
i := 2
s = append(s[:i], s[i+1:]...) // [1, 2, 4, 5]
删除索引 i 处的元素(保序)
s := []int{1, 2, 3, 4, 5}
i := 2
copy(s[i:], s[i+1:])
s = s[:len(s)-1] // [1, 2, 4, 5]
注意:删除后底层数组对应位置的值可能残留旧数据(内存泄漏风险),如果元素是指针或包含指针的结构体,应将末尾置零:
s[len(s)] = zeroValue // 或 s = s[:len(s)-1] 前先清零
7. 切片与底层数组的共享问题
这是切片最容易踩的坑:
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4] // [2, 3, 4]
s2 := arr[0:3] // [1, 2, 3]
s1[0] = 99 // arr 变成 [1, 99, 3, 4, 5]
// s2 也变成了 [1, 99, 3]
s1 和 s2 共享同一底层数组,修改一方会影响另一方。
避免共享:使用完整切片表达式
s1 := arr[1:4:4] // [low:high:max],cap = max - low = 3
// 此时 s1 的容量不会超过 arr[4],append 时会触发扩容,不再影响 arr
8. 切片作为函数参数
切片是值传递,但传递的是 header(指针 + len + cap),所以函数内修改元素会影响原切片:
func modify(s []int) {
s[0] = 99 // 影响原切片
}
func appendTo(s []int) []int {
s = append(s, 4) // 可能不影响原切片(扩容时)
return s
}
最佳实践: 需要扩容的场景,函数应返回新切片。
9. 切片的内存陷阱
9.1 大切片的小截取导致内存泄漏
var hugeSlice = make([]byte, 1<<20) // 1MB
small := hugeSlice[:10] // small 指向 hugeSlice 的起始位置
// 即使只用了 10 字节,1MB 的内存也无法被 GC 回收
解决: 用 copy 提取需要的部分
small := make([]byte, 10)
copy(small, hugeSlice[:10])
9.2 append 意外覆盖
s := []int{1, 2, 3, 4, 5}
t := s[:3] // [1, 2, 3],cap=5
t = append(t, 99) // 不扩容,直接写入 s[3]
// s 变成 [1, 2, 3, 99, 5]
10. 常用操作速查表
| 操作 | 代码 |
|---|---|
| 追加 | s = append(s, v) |
| 追加多个 | s = append(s, s2...) |
| 复制 | copy(dst, src) |
| 删除(保序) | copy(s[i:], s[i+1:]); s = s[:len(s)-1] |
| 删除(不保序) | s = append(s[:i], s[i+1:]...) |
| 插入(位置 i) | s = append(s[:i+1], s[i:]...); s[i] = v |
| 前插 | s = append([]int{v}, s...) |
| 过滤 | 原地过滤,维护写指针 |
| 反转 | for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] } |
11. 总结
- 切片 = 指针 + 长度 + 容量,是对数组的动态视图
append可能扩容,返回新切片,必须接收返回值- 切片截取会共享底层数组,注意相互影响
- 大切片的小截取会阻止 GC,需要
copy解绑 - 作为参数传递时是值传递 header,修改元素会影响原切片,但
append扩容后不会