Go 从 0 到精通 · 第 09 课:切片 slice
Go 从 0 到精通 · 第 09 课:切片 slice
学习定位:这是整套 Go 教程的第 9 课。
前置要求:已经完成第 8 课,理解数组的声明、初始化、遍历,知道数组是值类型、长度不可变。
本课目标:掌握切片的创建、长度与容量、append、切片截取、底层共享问题,理解切片为什么是 Go 日常开发中最常用的数据结构。
1. 本课你要解决的核心问题
第 8 课学的数组有两个硬伤:
- 长度固定,不能增减元素
- 长度是类型的一部分,函数签名绑死了长度
切片(slice)就是为了解决这些问题而设计的。它是 Go 中使用频率最高的数据结构。
你需要搞明白以下问题:
- 切片和数组的区别到底是什么
- 切片的长度和容量分别指什么
append怎么给切片加元素- 切片截取后,修改一个会影响另一个吗
- 切片的底层机制是什么
学完这一课,切片将替代数组成为你处理数据集合的默认选择。
2. 切片 vs 数组:一眼看懂区别
| 特性 | 数组 | 切片 |
|---|---|---|
| 长度 | 固定 | 动态可变 |
| 声明 | [5]int |
[]int |
| 类型 | 长度是类型的一部分 | 长度不是类型的一部分 |
| 添加元素 | 不支持 | append |
| 函数传参 | 每次绑死长度 | 灵活,任意长度 |
一句话总结:
数组是固定大小的容器,切片是动态大小的视图。
3. 切片的创建
3.1 用 []T{} 字面量创建
1 | package main |
注意声明方式的区别:
1 | a := [5]int{1, 2, 3, 4, 5} // 数组:有长度 [5] |
[]int 没有指定长度,所以是切片。
3.2 用 make 创建
make 是 Go 的内置函数,专门用于创建切片、map 和 channel:
1 | package main |
- 第二个参数
3:初始长度,元素都是零值 - 第三个参数
10:容量(可省略,省略时容量等于长度)
3.3 声明但不初始化
1 | var s []int // s 是 nil 切片 |
nil 切片长度为 0,可以安全地传给 append(append 会自动创建新底层数组)。
4. 长度 len 与容量 cap
这是切片最重要也最容易混淆的概念。
4.1 概念解释
- 长度
len:切片当前包含的元素个数 - 容量
cap:从切片的第一个元素开始,到底层数组末尾的元素个数
打个比方:
1 | 底层数组: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
切片看到 3 个元素(长度),但底层数组还有 7 个位置没用(容量 = 3 + 7 = 10)。
4.2 实际例子
1 | package main |
容量的实际意义:容量决定了在不重新分配内存的情况下,append 最多能加多少元素。
5. append:给切片添加元素
5.1 基本用法
1 | package main |
输出:
1 | 原始: [1 2 3] |
重要:append 返回一个新切片,你必须用返回值重新赋给变量。
1 | // 错误写法:丢弃了返回值 |
5.2 追加另一个切片
用 ... 展开一个切片,追加到另一个切片:
1 | a := []int{1, 2} |
这和第 6 课学的可变参数 ... 是同一个道理。
5.3 append 何时扩容
当 append 发现容量不够时,它会:
- 分配一块更大的内存
- 把旧数据复制过去
- 添加新元素
- 返回指向新内存的切片
扩容策略大致是:
- 容量 < 1024:翻倍
- 容量 >= 1024:增长约 25%
1 | package main |
输出(具体值可能因 Go 版本略有不同):
1 | i=0 len=1 cap=2 |
每次容量不够就翻倍。
6. 切片截取(Slicing)
6.1 基本语法
1 | s[low:high] |
从索引 low(包含)到 high(不包含)截取。
1 | package main |
省略规则:
s[:n]等价于s[0:n]s[n:]等价于s[n:len(s)]s[:]等价于s[0:len(s)]
6.2 截取会共享底层数组
这是切片最需要小心的地方:
1 | package main |
sub 和 original 共享同一个底层数组。修改 sub 会影响 original。
截取不会复制数据,它只是创建了一个新的"窗口",指向同一块内存的不同位置。
6.3 用 copy 真正复制
如果你不想要共享,用 copy 函数:
1 | package main |
copy(dst, src) 把 src 的元素复制到 dst。两个切片不再共享底层数组。
7. 切片作为函数参数
7.1 切片传参是"引用语义"的近似
1 | package main |
函数内部修改了切片的元素,外部也变了。
但这并不是真正的"引用传递"——Go 仍然是值传递。传给函数的是切片头(header)的副本,但副本和原切片共享同一个底层数组,所以通过下标修改元素会影响原数据。
7.2 append 在函数内不会影响外部
1 | package main |
输出:
1 | 函数内: [1 2 3 999] |
原因:append 可能返回一个新的切片(扩容时),但这个新切片只赋给了函数内的局部变量 s,不影响外部。
如果想让函数通过 append 修改外部切片,需要传指针 *[]int,或者让函数返回新切片。
7.3 推荐做法:返回新切片
1 | func addElement(s []int) []int { |
这也是 Go 标准库和社区的常见做法。
8. 删除切片中的元素
Go 没有内置的删除操作,但可以通过截取 + append 实现。
8.1 删除指定索引的元素
1 | package main |
原理:把下标 2 前面的部分 s[:2] 和下标 2 后面的部分 s[3:] 拼接起来。
8.2 注意:这会修改底层数组
上面的操作会改变底层数组的元素。如果你不希望影响原数据,应该先 copy 一份。
8.3 删除第一个和最后一个元素
1 | s := []int{0, 1, 2, 3, 4} |
9. nil 切片 vs 空切片
1 | var a []int // nil 切片 |
nil切片:没有底层数组,len为 0,== nil为true- 空切片:有底层数组(长度为 0),
len为 0,== nil为false
实际使用中,两者的行为几乎一样:都能 append、都能 for range(不执行循环体)。但 nil 切片更节省内存。
实践建议:不要区分它们的语义差异。大多数场景下,nil 切片和空切片可以互换使用。
10. 一段综合示例
1 | package main |
11. 常见坑总结
11.1 忘记接收 append 的返回值
1 | s := []int{1, 2} |
append 返回新切片,不赋值就等于白做。
11.2 截取后修改影响原切片
1 | a := []int{1, 2, 3, 4, 5} |
如果不想共享,用 copy 创建独立副本。
11.3 在循环中 append 可能导致意外
1 | slices := [][]int{} |
解决:每次 append 前 copy 一份。
11.4 切片传给函数后 append 不影响外部
1 | func add(s []int) { |
要影响外部,要么返回新切片,要么传 *[]int。
11.5 把切片和数组搞混
1 | a := [3]int{1, 2, 3} // 数组 |
看中括号里有没有数字:有数字是数组,没数字是切片。
12. 本课练习
练习 1:动态构建切片
要求:
- 从一个空切片开始
- 用
for循环和append添加 1 到 10 - 打印结果
练习 2:切片截取与共享
要求:
- 创建切片
a := []int{0, 1, 2, 3, 4, 5} - 截取
b := a[2:4] - 修改
b[0],打印a和b - 解释为什么
a也变了
练习 3:删除切片元素
要求:
- 实现函数
removeAt(s []int, index int) []int,删除指定下标的元素 - 不要越界,越界时返回原切片
- 测试删除第一个、中间、最后一个元素
练习 4:切片去重
要求:
- 实现函数
unique(s []int) []int - 返回去重后的新切片(保持顺序)
- 例如
[1, 2, 2, 3, 1]返回[1, 2, 3]
练习 5:观察 append 扩容
要求:
- 从容量为 1 的切片开始
- 循环
append20 个元素 - 每次打印
len和cap - 总结容量变化规律
练习 6:用 copy 实现独立副本
要求:
- 创建切片
original - 用
copy创建独立副本dup - 修改
dup,验证original不受影响
13. 自测题
13.1 概念题
- 切片和数组的核心区别是什么?
- 切片的
len和cap分别指什么? append返回什么?为什么必须接收返回值?- 切片截取后,新切片和原切片共享什么?
- 怎么创建不共享底层数组的切片副本?
nil切片和空切片有什么区别?- 切片传给函数后,在函数里
append会影响外部吗?为什么? make([]int, 3, 10)创建的切片,len和cap分别是多少?[]int{1, 2, 3}是数组还是切片?怎么判断?- 怎么用
append删除切片中指定下标的元素?
14. 本课总结
这一课你学到了 Go 中使用最广泛的数据结构——切片。
你现在应该已经理解:
- 切片是动态大小的序列,底层基于数组
- 用
[]T{}字面量或make([]T, len, cap)创建 len是当前元素个数,cap是底层数组的可用空间append添加元素,容量不够时自动扩容(近似翻倍)- 截取
s[low:high]不复制数据,新切片和原切片共享底层数组 - 用
copy(dst, src)创建独立副本 - 切片传参是值传递(切片头的副本),但共享底层数组
append在函数内不影响外部切片(可能返回新切片)
15. 下一课预告
下一课我们学习:映射 map。
会重点讲:
- map 是什么,和切片有什么区别
- map 的创建、增删改查
- 怎么判断键是否存在
- map 的遍历
- map 是引用类型——函数内外共享
学完下一课,你就能用键值对来组织和查找数据了。





