第5课:数组与切片
学习目标
完成本课学习后,你将能够:
- 理解Go语言中数组的声明、初始化和基本操作
- 掌握切片的概念及其与数组的核心区别
- 熟练使用
make和append函数操作切片 - 理解切片的工作原理,包括长度、容量和底层数组
- 为后续学习切片在并发编程中的安全使用打下基础
核心概念
1. 数组:固定大小的容器
想象一个储物柜,每个格子大小相同,总格子数在建造时就确定了。这就是数组——一个固定长度的、存放相同类型元素的数据结构。
// 声明并初始化一个长度为3的整型数组
var ages [3]int = [3]int{10, 20, 30}
数组长度是其类型的一部分,[3]int 和 [4]int 是不同的类型,不能互相赋值。这个“僵化”的特性有时会带来不便。
2. 切片:灵活的“窗口”
切片是基于数组的抽象,它更像一个动态窗口,提供三个关键信息:
- 指针:指向底层数组的某个元素
- 长度(Length):切片当前包含的元素数量,用
len(s)获取 - 容量(Capacity):从切片起始位置到底层数组末尾的元素数量,用
cap(s)获取
// 直接声明并初始化一个切片(Go会自动创建底层数组)
scores := []int{85, 90, 95}
// 从数组创建切片
numbers := [5]int{1, 2, 3, 4, 5}
slice1 := numbers[1:3] // 包含索引1和2,即[2, 3]
切片最强大的地方在于它可以动态增长,这是通过 append 函数实现的。
3. make 和 append:切片的创建与增长
make:创建切片时预分配空间,适合明确知道容量范围的场景append:向切片末尾追加元素,如果容量不足会自动扩容(通常创建新数组)
// 创建一个长度为0,容量为5的切片
data := make([]int, 0, 5)
// 追加元素
data = append(data, 10)
data = append(data, 20, 30)
关键点:append 可能返回新的切片(当发生扩容时),所以结果必须赋值给原变量。
代码示例
示例1:数组与切片的基本操作
package main
import "fmt"
func main() {
// 数组示例
var fruits [3]string = [3]string{"苹果", "香蕉", "橙子"}
fmt.Println("水果数组:", fruits)
fmt.Println("第一个水果:", fruits[0])
// 切片示例(直接创建)
vegetables := []string{"西红柿", "黄瓜", "茄子"}
fmt.Println("蔬菜切片:", vegetables)
// 切片操作
// 1. 获取部分切片
subVeg := vegetables[1:3]
fmt.Println("部分蔬菜:", subVeg) // [黄瓜, 茄子]
// 2. 追加元素
vegetables = append(vegetables, "土豆")
fmt.Println("追加后:", vegetables)
// 3. 检查长度和容量
fmt.Printf("长度: %d, 容量: %d\n", len(vegetables), cap(vegetables))
}
示例2:深入理解切片的工作原理
package main
import "fmt"
func main() {
// 创建底层数组
array := [5]int{10, 20, 30, 40, 50}
fmt.Println("原始数组:", array)
// 创建两个切片(共享同一个底层数组)
sliceA := array[1:3] // 索引1-2: [20, 30]
sliceB := array[2:4] // 索引2-3: [30, 40]
fmt.Println("切片A:", sliceA, "长度:", len(sliceA), "容量:", cap(sliceA))
fmt.Println("切片B:", sliceB, "长度:", len(sliceB), "容量:", cap(sliceB))
// 修改切片A会影响底层数组,进而影响切片B
sliceA[0] = 999
fmt.Println("修改切片A后:")
fmt.Println("切片A:", sliceA)
fmt.Println("切片B:", sliceB) // sliceB[0] 也会变成999
fmt.Println("底层数组:", array)
// 使用append可能导致扩容,创建新的底层数组
sliceA = append(sliceA, 1, 2, 3, 4, 5) // 追加多个元素,超出原容量
fmt.Println("append扩容后:")
fmt.Println("切片A:", sliceA)
fmt.Println("切片B:", sliceB) // 不受影响,因为扩容创建了新数组
fmt.Println("底层数组:", array) // 不受影响
}
示例3:使用make创建切片并管理容量
package main
import "fmt"
func main() {
// 预分配容量,避免频繁扩容
data := make([]int, 0, 10) // 长度0,容量10
// 填充数据
for i := 0; i < 15; i++ {
data = append(data, i*2)
fmt.Printf("添加%d后 - 长度:%d, 容量:%d\n", i*2, len(data), cap(data))
}
fmt.Println("最终切片:", data)
}
实践练习
练习1:基础操作(简单)
创建一个存储学生姓名的切片,完成以下操作:
- 初始包含:"张三", "李四", "王五"
- 追加:"赵六", "钱七"
- 删除索引为2的元素(王五)
- 打印最终结果
预期输出:
[张三 李四 赵六 钱七]
练习2:切片切片与修改(中等)
给定切片 data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
- 创建切片
firstHalf(前5个元素) - 创建切片
lastHalf(后5个元素) - 将
firstHalf的最后一个元素(5)改为 100 - 验证修改是否影响了原切片
data,并解释原因
练习3:并发场景模拟(挑战)
创建一个共享的整数切片,模拟两个并发任务:
- 任务A:循环100次,每次向切片追加一个随机数
- 任务B:循环100次,每次读取切片长度
- 运行程序,观察是否会出现问题(提示:切片并发读写不安全)
- 思考:为什么会出现问题?该如何解决?(提示:使用
sync.Mutex)
常见错误
1. 误以为切片是值传递
// 错误:认为函数内的修改不会影响原切片
func modifySlice(s []int) {
s = append(s, 100) // 这不会影响原切片
}
// 正确做法:返回修改后的切片
func modifySliceCorrect(s []int) []int {
return append(s, 100)
}
2. 切片扩容后的误解
original := make([]int, 3, 3) // [0, 0, 0]
s1 := original[:2] // 指向同一数组
s1 = append(s1, 100) // 扩容!创建新数组
s1[0] = 999 // 修改s1不影响original
// 此时:
// s1: [999, 0, 100](新数组)
// original: [0, 0, 0](原数组未变)
3. 切片索引越界
s := []int{1, 2, 3}
// 错误:访问超出范围的索引
// value := s[5] // 运行时panic
// 正确:先检查长度
if len(s) > 5 {
value := s[5]
}
4. 并发使用切片不加保护
var sharedSlice []int
// 错误:多个goroutine同时读写会导致数据竞争
go func() {
sharedSlice = append(sharedSlice, 1)
}()
go func() {
length := len(sharedSlice) // 可能读到不一致的数据
}()
小结
关键要点回顾
-
数组 vs 切片:
- 数组:固定长度,类型的一部分,适合大小固定的场景
- 切片:动态长度,引用类型,更常用且灵活
-
切片三要素:
- 指针:指向底层数组
- 长度:当前元素数量
- 容量:最大可存储元素数量
-
核心操作:
- 创建:
[]T{...}、make([]T, len, cap) - 追加:
append(slice, elements...) - 切片:
slice[low:high](左闭右开) - 复制:
copy(dst, src)
- 创建:
-
重要特性:
- 切片是引用类型,函数间传递成本低
- 共享切片可能共享底层数组,修改会相互影响
append可能创建新的底层数组(扩容时)- 切片不是并发安全的,这是后续并发编程的重要考点
下一步学习预告
在下一课中,我们将学习 Map(哈希表),它是另一种强大的数据结构。同时,我们将开始探讨为什么在并发编程中,切片和Map都需要特殊的保护机制。
练习编辑器
go
Loading...