Go 切片(Slice)

1. 什么是切片

切片是对底层数组的一个动态视图,包含三个要素:

字段类型说明
指针unsafe.Pointer指向底层数组中切片的起始位置
长度int切片中元素个数,len(s)
容量int从起始位置到底层数组末尾的元素个数,cap(s)

切片是引用类型,零值为 nilnil 切片的 lencap 都是 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))
  • dstsrc 的底层数组完全独立,互不影响
  • 深拷贝的惯用写法:
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 扩容后不会

曼波