上一篇我们学习了数组 Array。数组可以保存一组相同类型的数据,但是有一个明显限制:长度固定。
csharp
var nums [5]int
一旦定义为 [5]int,长度就是 5,运行过程中不能变成 6 或 10。但实际开发中的用户列表、商品列表、订单数据、接口返回结果等,数量通常是不确定的。
因此 Go 提供了更加灵活的数据结构------切片 Slice 。
切片是 Go 开发中使用频率最高的数据结构之一,理解 Slice 对后面学习 Map、函数参数、JSON、数据库查询等内容都非常重要。
一、什么是 Slice
Slice 中文通常叫"切片",它可以理解为对底层数组的一段动态描述。
数组:
csharp
var nums [5]int
切片:
csharp
var nums []int
两者最大的语法区别就是有没有长度。
csharp
[5]int // 数组
[]int // 切片
数组长度属于类型的一部分,而 Slice 本身不固定长度。
例如:
go
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
fmt.Println(nums)
}
输出:
csharp
[10 20 30]
Slice 可以继续增加元素,因此比数组更加适合实际业务开发。
二、Slice 的基本定义
定义一个 Slice:
csharp
var nums []int
此时 nums 的值是 nil。
ini
fmt.Println(nums == nil)
输出:
arduino
true
虽然是 nil Slice,但仍然可以获取长度:
go
fmt.Println(len(nums))
输出:
0
也可以直接初始化:
go
nums := []int{10, 20, 30}
names := []string{"Tom", "Jack", "Lucy"}
注意:
go
nums := [3]int{1, 2, 3}
这是数组。
而:
go
nums := []int{1, 2, 3}
才是 Slice。
三、使用 make 创建 Slice
除了直接初始化,还可以使用 make() 创建 Slice。
基本语法:
scss
make([]类型, 长度)
例如:
go
nums := make([]int, 5)
fmt.Println(nums)
输出:
csharp
[0 0 0 0 0]
创建出来的元素会自动使用对应类型的零值。
还可以同时指定容量:
go
nums := make([]int, 5, 10)
这里:
ini
长度 length = 5
容量 capacity = 10
可以分别查看:
go
fmt.Println(len(nums))
fmt.Println(cap(nums))
输出:
5
10
len() 获取当前元素长度,cap() 获取当前 Slice 从起始位置到底层数组末尾能够容纳的元素数量。
四、向 Slice 添加元素
Slice 最重要的能力之一就是可以动态添加元素。
Go 使用 append():
go
nums := []int{10, 20, 30}
nums = append(nums, 40)
fmt.Println(nums)
输出:
csharp
[10 20 30 40]
继续添加:
ini
nums = append(nums, 50)
nums = append(nums, 60)
也可以一次添加多个:
ini
nums = append(nums, 40, 50, 60)
需要特别注意:
scss
append(nums, 40)
并不会自动修改变量 nums,应该写成:
ini
nums = append(nums, 40)
因为 append() 会返回新的 Slice。
五、追加另一个 Slice
假设有两个 Slice:
css
a := []int{1, 2, 3}
b := []int{4, 5, 6}
可以把 b 添加到 a:
css
a = append(a, b...)
结果:
csharp
[1 2 3 4 5 6]
这里的:
css
b...
表示把 b 中的元素展开后传递给 append()。
字符串 Slice 同样可以:
go
a := []string{"Go", "Java"}
b := []string{"Python", "Rust"}
a = append(a, b...)
六、访问和修改 Slice
Slice 和数组一样,通过下标访问元素:
css
nums := []int{10, 20, 30}
fmt.Println(nums[0])
fmt.Println(nums[1])
修改元素:
scss
nums[1] = 100
fmt.Println(nums)
输出:
csharp
[10 100 30]
下标同样从 0 开始。
如果:
scss
len(nums) == 3
有效下标就是:
0 1 2
访问:
css
nums[3]
会发生越界错误。
七、遍历 Slice
普通 for 循环:
css
nums := []int{10, 20, 30, 40}
for i := 0; i < len(nums); i++ {
fmt.Println(nums[i])
}
实际开发中更常见的是 range:
go
for index, value := range nums {
fmt.Println(index, value)
}
只需要值:
go
for _, value := range nums {
fmt.Println(value)
}
只需要下标:
go
for index := range nums {
fmt.Println(index)
}
如果需要修改 Slice 元素,可以通过下标:
css
for i := range nums {
nums[i] *= 2
}
不要写成:
go
for _, value := range nums {
value *= 2
}
因为 value 是当前元素值的副本,不会修改 Slice 中原来的元素。
八、Slice 的切片操作
Slice 名字的来源,就是它可以非常方便地"切"出一部分数据。
例如:
go
nums := []int{10, 20, 30, 40, 50}
取下标 1 到 3:
dart
part := nums[1:4]
fmt.Println(part)
输出:
csharp
[20 30 40]
切片范围遵循:
sql
[start:end)
包含 start,不包含 end。
因此:
css
nums[1:4]
实际取得:
css
nums[1]
nums[2]
nums[3]
还可以省略开始位置:
css
nums[:3]
相当于:
css
nums[0:3]
省略结束位置:
css
nums[2:]
表示从下标 2 一直到最后。
全部:
css
nums[:]
九、Slice 和底层数组
Slice 本身并不是完整保存所有数据的独立数组,它通常引用一个底层数组。
例如:
go
nums := []int{10, 20, 30, 40, 50}
part := nums[1:4]
part[0] = 100
fmt.Println(nums)
输出:
csharp
[10 100 30 40 50]
为什么修改 part 会影响 nums?
因为两者引用了同一块底层数组数据。
可以简单理解为:
css
nums -> [10 20 30 40 50]
↑
part ------+
part 并没有立即复制一份 [20 30 40],而是引用原来的数据。
这是 Slice 和数组非常重要的区别。
十、Slice 的长度和容量
Slice 有两个重要属性:
scss
len(slice)
cap(slice)
例如:
css
nums := make([]int, 3, 5)
fmt.Println(len(nums))
fmt.Println(cap(nums))
输出:
3
5
可以简单理解为:
ini
len = 当前可以直接访问的元素数量
cap = 当前底层空间可容纳的范围
当不断执行:
ini
nums = append(nums, value)
如果容量足够,可以继续使用当前底层数组;如果容量不足,Go 会根据运行时策略申请新的底层存储空间并复制数据。
因此我们通常不需要自己手动处理 Slice 扩容。
十一、提前分配容量
如果已经知道大概需要添加多少元素,可以提前指定容量。
例如需要添加 1000 条数据:
css
nums := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
nums = append(nums, i)
}
这里:
scss
len(nums) == 0
cap(nums) == 1000
随后不断 append()。
这样可以减少扩容和数据复制,在大量数据场景下更加高效。
这是实际 Go 项目中非常常见的写法。
十二、复制 Slice
如果希望得到一份真正独立的数据,可以使用 copy()。
例如:
css
a := []int{10, 20, 30}
b := make([]int, len(a))
copy(b, a)
b[0] = 100
fmt.Println(a)
fmt.Println(b)
输出:
csharp
[10 20 30]
[100 20 30]
这时候修改 b 就不会影响 a。
copy() 返回实际复制的元素数量:
css
n := copy(b, a)
fmt.Println(n)
十三、删除 Slice 中的元素
Go 没有提供专门的 deleteSlice() 函数,但可以利用切片和 append() 删除元素。
例如:
go
nums := []int{10, 20, 30, 40, 50}
删除下标 2 的 30:
css
nums = append(nums[:2], nums[3:]...)
结果:
csharp
[10 20 40 50]
原理就是:
css
nums[:2] -> [10 20]
nums[3:] -> [40 50]
然后重新组合。
十四、清空 Slice
如果想清空 Slice,可以:
ini
nums = nil
此时:
go
len(nums) == 0
nums == nil
也可以:
ini
nums = nums[:0]
此时长度也是 0,但通常仍然保留原来的底层存储空间。
两种方式需要根据具体场景选择。
十五、Slice 作为函数参数
实际 Go 开发中,经常把 Slice 作为函数参数:
go
func printNums(nums []int) {
for _, value := range nums {
fmt.Println(value)
}
}
调用:
go
nums := []int{10, 20, 30}
printNums(nums)
函数也可以修改 Slice 中已有的元素:
go
func change(nums []int) {
nums[0] = 100
}
func main() {
nums := []int{10, 20, 30}
change(nums)
fmt.Println(nums)
}
输出:
csharp
[100 20 30]
这是因为函数参数虽然会复制 Slice 描述信息,但复制后的 Slice 仍然可以引用同一个底层数组。
不过如果函数内部执行 append() 导致重新分配底层数组,情况就会有所不同。因此如果函数需要追加元素,通常直接返回新的 Slice:
go
func add(nums []int, value int) []int {
return append(nums, value)
}
调用:
ini
nums = add(nums, 40)
十六、二维 Slice
Slice 也可以嵌套:
go
nums := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
访问:
css
fmt.Println(nums[1][2])
输出:
6
遍历:
go
for _, row := range nums {
for _, value := range row {
fmt.Printf("%d ", value)
}
fmt.Println()
}
与二维数组相比,二维 Slice 更加灵活,每一行的长度甚至可以不同:
go
nums := [][]int{
{1, 2},
{3, 4, 5},
{6},
}
十七、数组和 Slice 的区别
数组:
csharp
[5]int
Slice:
arduino
[]int
主要区别可以概括为:
go
数组 Slice
长度固定 长度动态变化
长度属于类型的一部分 长度不属于类型
赋值复制整个数组 复制 Slice 描述信息
不能直接 append 可以 append
适合固定数量数据 适合动态数据
实际 Go 项目中,Slice 的使用频率远高于数组。
十八、Slice 实战
假设需要保存一组商品价格,并计算总价:
go
package main
import "fmt"
func main() {
prices := []float64{99.9, 199, 59.9}
prices = append(prices, 299)
var total float64
for _, price := range prices {
total += price
}
fmt.Println("商品数量:", len(prices))
fmt.Println("商品总价:", total)
}
Slice 特别适合用户列表、商品列表、订单列表、接口结果、数据库查询结果等数量不确定的数据。
十九、Slice 常见错误
第一个是忘记接收 append() 返回值:
scss
append(nums, 100)
应该:
ini
nums = append(nums, 100)
第二个是切片后误以为数据已经复制:
css
b := a[1:3]
b 通常仍然引用 a 的底层数组。
第三个是访问越界:
css
nums[len(nums)]
最后一个有效下标应该是:
css
nums[len(nums)-1]
第四个是混淆长度和容量:
scss
len(nums)
cap(nums)
长度决定当前可以直接访问多少元素,容量描述 Slice 从当前位置到底层数组末尾可利用的空间。
二十、总结
Slice 是 Go 中最常用的数据集合之一。
定义:
csharp
var nums []int
初始化:
go
nums := []int{1, 2, 3}
创建:
go
nums := make([]int, 0, 10)
添加元素:
ini
nums = append(nums, 4)
获取长度:
scss
len(nums)
获取容量:
scss
cap(nums)
截取:
go
part := nums[1:3]
复制:
scss
copy(dst, src)
遍历:
go
for _, value := range nums {
fmt.Println(value)
}
学习 Slice 最需要掌握四个核心点:
markdown
1. Slice 长度可以动态变化
2. append 用于追加元素
3. Slice 通常引用底层数组
4. len 表示长度,cap 表示容量
数组解决的是"固定数量的数据",Slice 解决的是"动态数量的数据"。但无论数组还是 Slice,我们主要都是通过数字下标访问数据,例如:
bash
users[0]
users[1]
如果我们希望通过名字、ID、配置项名称等快速找到对应的数据,例如:
rust
"张三" -> 90
"李四" -> 85
"port" -> 8080
"host" -> localhost
就需要使用 Go 中另一种非常重要的数据结构------Map。
下一篇:Map------使用 Key-Value 管理键值数据