目录
前言
引子
一、切片是什么?
二、创建切片的方法
1. 通过数组生成切片
2. 使用字面量直接创建切片
3. 使用 make 函数创建切片
4. 声明 nil 切片
三、切片的长度和容量
四、切片的动态增长 —— append 函数
五、切片的复制 —— copy 函数
六、切片的遍历
七、切片是引用类型
八、切片的扩容机制
九、切片表达式的高级用法
十、nil 切片和空切片
十一、切片作为函数参数
十二、综合示例:管理学生名单
十三、数组与切片的对比总结
十四、常见陷阱与注意事项
总结
前言
在上一篇文章中,我们学习了数组。数组是固定长度的,一旦声明就无法改变大小。但在实际开发中,我们常常需要动态地增加或删除元素,比如一个在线商城的购物车列表、用户的消息记录等。为了解决这个问题,Go语言提供了切片(Slice)。
切片可以看作是动态数组,它比数组更灵活、更常用。可以说,在Go语言中,切片才是处理序列数据的“主力军”。今天,我们就来深入理解切片:它的底层原理、如何创建和使用,以及它与数组的区别。
引子
因为数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性。 例如:
func arraySum(x [3]int) int{
sum := 0
for _, v := range x{
sum = sum + v
}
return sum
}
这个求和函数只能接受[3]int类型,其他的都不支持。 再比如:
a := [3]int{1, 2, 3}
数组a中已经有三个元素了,我们不能再继续往数组a中添加新元素了。
切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。
一、切片是什么?
切片是对数组的一个连续片段的引用。它本身不存储任何数据,只是底层数组的一个“窗口”。切片的长度可以动态变化,但底层数组的容量是固定的,当切片长度超过容量时,Go会自动分配一个新的更大的底层数组。
切片由三个部分组成:
-
指针:指向底层数组的起始位置
-
长度(length):切片中当前元素的个数
-
容量(capacity):从切片起始位置到底层数组末尾的元素个数
可以这样理解:切片是一个“视图”,让我们能够像操作动态数组一样操作数据。
二、创建切片的方法
1. 通过数组生成切片
可以通过 [low:high] 操作从一个数组(或另一个切片)上切出一段:
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // 包含索引 1,2,3 → [2,3,4]
fmt.Println(s) // [2 3 4]
切片语法:[start:end],左闭右开,即包含 start 索引,不包含 end 索引。省略 start 表示从0开始,省略 end 表示到末尾。
s1 := arr[:3] // [1,2,3]
s2 := arr[2:] // [3,4,5]
s3 := arr[:] // 整个数组的切片 [1,2,3,4,5]
2. 使用字面量直接创建切片
创建切片不需要指定长度(区别于数组需要指定长度或用 …):
// 数组写法
arr := [3]int{1,2,3} // 有长度
// 切片写法
s := []int{1,2,3} // 没有长度,只有方括号
fmt.Printf("%T\\n", s) // []int
字面量方式会在底层创建一个数组,并返回一个引用该数组的切片。
3. 使用 make 函数创建切片
make 是创建切片最灵活的方式,可以指定长度和容量:
s := make([]int, 5) // 长度5,容量也是5,初始值为[0,0,0,0,0]
s2 := make([]int, 3, 5) // 长度3,容量5
make 创建时底层会分配一个数组,所有元素初始化为零值。
4. 声明 nil 切片
var s []int // 未初始化的切片,值为 nil
if s == nil {
fmt.Println("s is nil")
}
nil 切片的长度和容量都是0,但没有底层数组。我们可以直接对 nil 切片使用 append 函数,Go会为其分配底层数组。
三、切片的长度和容量
-
长度:切片当前包含的元素个数,用 len(s) 获取。
-
容量:从切片起始位置到底层数组末尾的元素个数,用 cap(s) 获取。
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4] // [20,30,40]
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 4(从arr[1]到arr[4]共4个元素)
切片的容量决定了在不重新分配底层数组的前提下,切片最多能增长多少。
四、切片的动态增长 —— append 函数
append 是往切片末尾追加元素的内置函数。如果原切片容量足够,它会直接修改底层数组并返回原切片;如果容量不足,Go会分配一个新的底层数组(容量通常翻倍),复制原有元素,然后追加新元素。
s := []int{1, 2, 3}
s = append(s, 4) // [1,2,3,4]
s = append(s, 5, 6) // [1,2,3,4,5,6]
重要:append 返回一个新的切片(可能指向同一个底层数组,也可能是新的),所以必须用原变量接收返回值。
同时可以追加另一个切片,使用 … 解包:
s1 := []int{1,2}
s2 := []int{3,4}
s1 = append(s1, s2…)
fmt.Println(s1) // [1,2,3,4]
五、切片的复制 —— copy 函数
copy 可以将一个切片的数据复制到另一个切片中,复制长度以较小的那个为准。它返回成功复制的元素个数。
src := []int{1,2,3}
dst := make([]int, 2)
n := copy(dst, src) // 将src的前两个元素复制到dst
fmt.Println(dst) // [1,2]
fmt.Println(n) // 2
如果希望完整复制一个独立的切片(深拷贝),需要先 make 一个长度足够的目标切片,然后 copy。
original := []int{1,2,3}
clone := make([]int, len(original))
copy(clone, original)
clone[0] = 100
fmt.Println(original) // [1,2,3] 原切片不受影响
六、切片的遍历
切片的遍历与数组完全一致,可以使用 for 索引或 for range:
s := []string{"Go", "Python", "Java"}
for i, v := range s {
fmt.Printf("索引%d: %s\\n", i, v)
}
// 只需要值
for _, v := range s {
fmt.Println(v)
}
七、切片是引用类型
这一点是切片与数组最大的区别。切片本身是对底层数组的引用。当你将切片赋值给另一个变量或作为函数参数传递时,传递的是引用(其实是一个包含指针、长度、容量的结构体副本,但底层数组指针相同),因此修改切片元素会影响原切片。
func modify(s []int) {
s[0] = 100
}
func main() {
arr := [3]int{1,2,3}
s := arr[:]
modify(s)
fmt.Println(arr) // [100,2,3] 底层数组被修改
}
由于切片是引用类型,传递大切片作为参数非常高效(只拷贝3个机器字),而不会像数组那样拷贝整个元素。
八、切片的扩容机制
当使用 append 导致切片长度超过容量时,Go会分配一个新的底层数组。扩容规则大致是:
-
如果原切片容量小于1024,新容量会翻倍;
-
如果原切片容量大于等于1024,新容量会增长1.25倍左右(具体实现可能调整)。
这个策略是为了平衡内存分配和复制开销。我们可以通过代码观察:
s := make([]int, 0, 1) // 初始容量1
oldCap := cap(s)
for i := 0; i < 10; i++ {
s = append(s, i)
if newCap := cap(s); newCap != oldCap {
fmt.Printf("容量变化: %d → %d\\n", oldCap, newCap)
oldCap = newCap
}
}
输出示例:
容量变化: 1 → 2
容量变化: 2 → 4
容量变化: 4 → 8
容量变化: 8 → 16
九、切片表达式的高级用法
除了基本的 [low:high],Go还支持带容量的切片表达式:[low:high:max]。其中 max 用来限制新切片的容量,即容量 = max – low。这样可以将切片的容量限制在底层数组的一部分。
arr := [5]int{0,1,2,3,4}
s := arr[1:3:4] // 长度=2(索引1,2),容量=3(索引1,2,3)
fmt.Println(len(s), cap(s)) // 2 3
十、nil 切片和空切片
-
nil 切片:var s []int,没有底层数组,len=0, cap=0,s == nil 为 true。
-
空切片:s := []int{} 或 s := make([]int,0),有底层数组(可能是共享的空数组),len=0, cap=0,但 s != nil。
在大多数情况下,函数返回切片时可以返回 nil 切片,因为它可以直接用 append 等操作。判断切片是否为空推荐使用 len(s) == 0,而不是与 nil 比较。
var s1 []int // nil切片
s2 := []int{} // 非nil空切片
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
十一、切片作为函数参数
因为切片是引用类型,在函数内部修改切片元素会影响外部。但如果要修改切片本身(如通过 append 改变长度或容量),则需要传入切片指针或返回新切片。
func addElement(s []int, val int) []int {
s = append(s, val) // 可能改变底层数组
return s
}
func main() {
s := []int{1,2}
s = addElement(s, 3) // 必须接收返回值
fmt.Println(s) // [1,2,3]
}
十二、综合示例:管理学生名单
下面用切片实现一个简单的学生名单管理,演示动态增加、删除、遍历等功能。
package main
import "fmt"
func main() {
// 初始化学生切片
students := []string{"张三", "李四", "王五"}
// 添加学生
students = append(students, "赵六", "孙七")
fmt.Println("名单:", students)
// 删除索引为2的学生(王五)
index := 2
students = append(students[:index], students[index+1:]…)
fmt.Println("删除后:", students)
// 遍历打印
for i, name := range students {
fmt.Printf("%d. %s\\n", i+1, name)
}
}
输出:
名单: [张三 李四 王五 赵六 孙七]
删除后: [张三 李四 赵六 孙七]
1. 张三
2. 李四
3. 赵六
4. 孙七
十三、数组与切片的对比总结
| 长度 | 固定,类型的一部分 | 动态,不属于类型 |
| 类型表示 | [n]T | []T |
| 值类型/引用类型 | 值类型(拷贝整个数组) | 引用类型(拷贝指针) |
| 初始化 | 指定长度或… | 不指定长度,或用make |
| 作为函数参数 | 效率低(大数组拷贝) | 效率高(只拷贝描述符) |
| 是否可比较(==) | 可比较(同类型) | 只能与nil比较 |
| 适用场景 | 固定大小、值语义 | 动态序列、大多数情况 |
在实际开发中,99%的情况下使用切片代替数组。数组通常用于底层数据结构或需要值语义的特殊场景。
十四、常见陷阱与注意事项
切片不直接拥有数据:多个切片可以共享同一个底层数组,修改一个会影响另一个。小心“别名”带来的意外。
s1 := []int{1,2,3,4}
s2 := s1[1:3] // [2,3]
s2[0] = 99
fmt.Println(s1) // [1,99,3,4] 原切片也被修改
append 的返回值必须接收:因为底层数组可能重新分配,原切片指向的数组可能失效。
切片的复制是浅拷贝:通过 copy 复制元素是深拷贝,但直接赋值 s2 := s1 只是拷贝了描述符,共享底层数组。
遍历时并发修改不安全:对切片同时读写(例如在 range 中修改切片长度)会有风险,需注意同步。
总结
今天我们学习了Go语言中最常用的序列类型——切片。
-
切片是动态数组的抽象,通过指针、长度、容量来引用底层数组。
-
创建切片的方式:数组切片、字面量、make、nil切片。
-
常用操作:len、cap、append、copy、切片表达式。
-
切片是引用类型,传递效率高,但要留意共享底层数组带来的副作用。
-
使用 append 时一定要接收返回值,因为可能发生内存扩容。
-
在绝大多数情况下,切片替代数组都是更好的选择。
掌握了切片,你就掌握了Go语言中数据容器的核心。下一篇文章,我们将继续学习 map(映射),它用于存储键值对,是切片之外的另一个常用数据结构。
如果你对今天的讲解有任何疑问,或者想进一步探索,欢迎在评论区留言。坚持从零开始,我们一步步掌握Go语言!
感谢阅读,我们下篇再见!
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!




