重要
切片底层是 ptr + len + cap 三元组。理解容量变化规则(翻倍扩容、子切片共享底层数组)是避免内存泄漏和意外修改的关键。
1. 创建
| 方式 | 代码 | len | cap | 说明 |
|---|
| 字面量 | s := []int{1, 2, 3} | 3 | 3 | 最常用 |
| make | s := make([]int, 5, 10) | 5 | 10 | 预分配容量,减少扩容 |
| nil 切片 | var s []int | 0 | 0 | nil,s == nil 为 true |
| 空切片 | s := []int{} | 0 | 0 | 非 nil,JSON 序列化为 [] |
1
2
| var nilSlice []int // nil
emptySlice := make([]int, 0) // 非 nil,len=0, cap=0
|
对外 API 返回空切片而非 nil——json.Marshal 对 nil 输出 null,空切片输出 []。
2. 追加
1
2
3
4
| s := []int{1, 2}
s = append(s, 3) // [1 2 3]
s = append(s, 4, 5, 6) // [1 2 3 4 5 6]
s = append(s, other...) // 展开另一个切片
|
扩容规则: 容量不足时自动扩容。Go 1.18 之前翻倍策略,之后改为更平滑的增长曲线。扩容时会分配新底层数组并复制数据。
1
2
3
4
| s := make([]int, 0, 2)
fmt.Println(cap(s)) // 2
s = append(s, 1, 2, 3)
fmt.Println(cap(s)) // > 2,已扩容
|
3. 删除
Fast版:改变顺序(O(1))
将最后一个元素移到被删位置,不保持原顺序:
1
2
3
4
5
6
| a := []string{"A", "B", "C", "D", "E"}
i := 2 // 删除 "C"
a[i] = a[len(a)-1] // 末位移到 i
a = a[:len(a)-1] // 截断
// [A B E D]
|
Slow版:保持顺序(O(n))
被删元素之后的元素全部前移一位:
1
2
3
4
5
6
| a := []string{"A", "B", "C", "D", "E"}
i := 2 // 删除 "C"
copy(a[i:], a[i+1:]) // C 被 D 覆盖,D 被 E 覆盖
a = a[:len(a)-1] // 截断
// [A B D E]
|
泛型版本(Go 1.18+)
1
2
3
| func Delete[T any](s []T, i int) []T {
return append(s[:i], s[i+1:]...)
}
|
4. 插入
1
2
3
4
5
6
7
| // 在位置 i 插入元素 x
s = append(s[:i], append([]int{x}, s[i:]...)...)
// Go 1.17+ 可以用 copy 避免临时切片
s = append(s, 0) // 扩展一个位置
copy(s[i+1:], s[i:]) // i 之后的元素后移
s[i] = x
|
1
2
3
4
5
6
| func Insert[T any](s []T, i int, x T) []T {
s = append(s, *new(T)) // Go 1.21: clear 更好
copy(s[i+1:], s[i:])
s[i] = x
return s
}
|
5. 过滤
1
2
3
4
5
6
7
8
9
| // 就地过滤,保留满足条件的元素
n := 0
for _, v := range s {
if predicate(v) {
s[n] = v
n++
}
}
s = s[:n]
|
1
2
3
4
5
6
7
8
9
10
| // 过滤偶数
nums := []int{1, 2, 3, 4, 5, 6}
n := 0
for _, v := range nums {
if v%2 == 0 {
nums[n] = v
n++
}
}
nums = nums[:n] // [2 4 6]
|
6. 子切片与容量
1
2
| s := make([]int, 3, 6) // len=3, cap=6
sub := s[1:2] // len=1, cap=5 (从 s[1] 到 s 末尾)
|
子切片与原切片共享底层数组:
1
2
3
4
5
| a := []int{1, 2, 3, 4, 5}
b := a[1:3] // [2 3],cap=4,指向 a 底层数组的索引 1 位置
b[0] = 99
fmt.Println(a) // [1 99 3 4 5] ← a 也被修改了!
|
避免共享:完整切片表达式
1
2
| b := a[1:3:3] // len=2, cap=2。第三个参数限制 cap
// 后续 append(b, ...) 必然触发扩容,分配新数组,不再影响 a
|
子切片 + append 是常见的内存泄漏/意外修改来源。如果子切片生命周期长但容量很大,用 s[low:high:max] 限制 cap。
7. 拷贝
1
2
3
| src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // 深拷贝
|
copy 按 min(len(dst), len(src)) 复制元素:
1
2
3
| dst := make([]int, 2)
n := copy(dst, []int{1, 2, 3, 4})
fmt.Println(dst, n) // [1 2] 2
|
8. 清空(Go 1.21+)
1
| clear(s) // 所有元素置零值,len 不变
|
旧版本等价写法:
1
2
3
| for i := range s { s[i] = T{} }
// 或
s = s[:0] // 重置 len 为 0,保留底层数组和 cap
|
9. 常见陷阱
9.1 range 中的值拷贝
1
2
3
4
5
6
7
8
9
10
| s := []User{{"A"}, {"B"}}
for _, u := range s {
u.Name = "X" // 修改的是副本,不影响 s
}
fmt.Println(s) // [{A} {B}]
// 正确做法
for i := range s {
s[i].Name = "X"
}
|
9.2 append 返回值必须接收
1
2
3
| s := make([]int, 0)
append(s, 1) // 错:返回值被丢弃
s = append(s, 1) // 对
|
9.3 子切片导致的内存泄漏
1
2
3
4
5
6
7
8
| // 大切片的一个小窗口,但底层大数组无法 GC
big := readMillionLines()
first10 := big[:10]
// 即使只用 10 行,百万行的底层数组依然存在
// 修复:拷贝需要的部分
first10 := make([]Line, 10)
copy(first10, big[:10])
|
10. 总结
| 操作 | 代码 | 注意 |
|---|
| 创建带容量 | make([]T, 0, capacity) | 预估容量避免频繁扩容 |
| 追加 | s = append(s, x) | 必须接收返回值 |
| 删除(保序) | copy(s[i:], s[i+1:]); s = s[:len(s)-1] | O(n) |
| 删除(不保序) | s[i] = s[len(s)-1]; s = s[:len(s)-1] | O(1) |
| 子切片 | s[low:high:max] | 用三元限制 cap 避免共享 |
| 深拷贝 | dst := make([]T, len(src)); copy(dst, src) | |
| 过滤 | 双指针法 | O(n),就地 |
参考链接