目录
Please enable Javascript to view the contents

Go-Slice-切片操作汇总

 ·  ☕ 3 分钟

重要

切片底层是 ptr + len + cap 三元组。理解容量变化规则(翻倍扩容、子切片共享底层数组)是避免内存泄漏和意外修改的关键。

1. 创建

方式代码lencap说明
字面量s := []int{1, 2, 3}33最常用
makes := make([]int, 5, 10)510预分配容量,减少扩容
nil 切片var s []int00nil,s == nil 为 true
空切片s := []int{}00非 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),就地

参考链接

分享

Hex
作者
Hex
CloudNative Developer