Go 编程实战:切片 Slice——灵活的动态数据集合

上一篇我们学习了数组 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}

删除下标 230

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 管理键值数据

相关推荐
再吃一根胡萝卜8 小时前
微服务治理的“四大护法”:从 Django 视角理解 Spring Cloud 核心组件
后端
再吃一根胡萝卜8 小时前
面试终极挑战:如何用 Django 经验,回答 Java 微服务实战问题?
后端
To_OC9 小时前
装完 ESLint 它一声不吭?我还以为代码写得多好
后端·node.js·eslint
凤山老林10 小时前
精细化流量治理:Spring Boot 动态特性开关与灰度发布体系
java·spring boot·后端
vipbic11 小时前
一个前端的 9 天重构:我是怎么用 Codex 重做航栈的
前端·javascript·后端
用户9385156350711 小时前
TypeScript 高级类型 + CSS 三列布局:从类型体操到样式工程的进阶之路
css·面试·typescript
考虑考虑13 小时前
Excel导入时产生特殊字符处理
java·后端·java ee
Scabbards_14 小时前
面试Leetcode - Heap 堆
java·leetcode·面试
IT_陈寒14 小时前
Redis的DEL命令竟然没删掉数据?我踩的这个坑你得知道
前端·人工智能·后端