01 数组与切片
本节目标:了解数组,重点掌握切片(slice)——Go 中使用频率最高的数据结构。
一、数组:定长的序列
数组是固定长度的同类型元素序列:
var arr [3]int // 长度为 3 的 int 数组,初始值 [0 0 0]
arr[0] = 10 // 通过下标访问(从 0 开始)
arr[1] = 20
nums := [3]int{1, 2, 3} // 声明并初始化
auto := [...]int{1, 2, 3, 4} // 让编译器数长度,len(auto) == 4
fmt.Println(len(arr)) // 3
数组的两个重要特性(也是它不常用的原因):
- 长度是类型的一部分:
[3]int和[4]int是两种不同的类型,不能互相赋值 - 赋值和传参会复制整个数组:
a := [3]int{1, 2, 3}
b := a // b 是 a 的完整拷贝
b[0] = 100
fmt.Println(a) // [1 2 3],a 没变
实际开发中直接用数组的场景很少,95% 的情况用切片。 数组了解到这里就够了。
二、切片:可变长的序列(重点!)
切片(slice)就像"可以变长的数组",是 Go 最常用的数据结构:
// 声明切片:和数组的区别就是 [] 里不写长度
var s []int // nil 切片,长度 0
nums := []int{1, 2, 3} // 声明并初始化
names := []string{"张三", "李四"}
基本操作
nums := []int{10, 20, 30}
// 访问和修改
fmt.Println(nums[0]) // 10
nums[1] = 200
// 长度
fmt.Println(len(nums)) // 3
// 遍历
for i, v := range nums {
fmt.Println(i, v)
}
// 越界会导致程序崩溃(panic),注意下标范围
// fmt.Println(nums[99]) // panic: index out of range
append:追加元素(最重要的操作)
var s []int // 空切片
s = append(s, 1) // [1]
s = append(s, 2, 3) // [1 2 3],可以一次追加多个
other := []int{4, 5}
s = append(s, other...) // [1 2 3 4 5],追加另一个切片要加 ...
⚠️ 必须写 s = append(s, ...),把结果赋回去! append 返回新切片,只写 append(s, 1) 不接收结果是没有效果的(而且编译器会报错提醒你)。
切片操作:从现有序列"切"一段
语法 s[开始:结束],含开始、不含结束(左闭右开):
s := []int{0, 1, 2, 3, 4, 5}
fmt.Println(s[1:4]) // [1 2 3](下标 1、2、3)
fmt.Println(s[:3]) // [0 1 2](从头开始)
fmt.Println(s[3:]) // [3 4 5](切到末尾)
fmt.Println(s[:]) // [0 1 2 3 4 5](完整切片)
用 make 创建指定长度的切片
s := make([]int, 3) // 长度 3,元素都是零值:[0 0 0]
s2 := make([]int, 0, 10) // 长度 0,容量 10(预留了 10 个位置)
三、理解切片的本质(新手最容易懵的地方)
切片本身不存数据,它是一个指向底层数组的"窗口",包含三个信息:指针、长度(len)、容量(cap)。
这带来一个重要结果:切片赋值/传参共享底层数组:
a := []int{1, 2, 3}
b := a // a、b 指向同一个底层数组!
b[0] = 100
fmt.Println(a) // [100 2 3] —— a 也变了!(对比数组:数组不会)
从切片上切出的子切片也共享数据:
s := []int{1, 2, 3, 4, 5}
sub := s[1:3] // [2 3]
sub[0] = 999
fmt.Println(s) // [1 999 3 4 5]
想要独立副本?用 copy
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a) // 把 a 的内容复制到 b
b[0] = 100
fmt.Println(a) // [1 2 3],a 不受影响
关于容量(cap)和扩容机制:append 超过容量时会分配新的底层数组。新手阶段先记住两条实用结论:
- 切片赋值共享数据,要独立副本用
copyappend的结果必须赋值回原变量
四、常用操作模式
// 判断切片是否为空
if len(s) == 0 { ... }
// 删除下标 i 的元素(Go 没有内置 delete,用拼接实现)
s = append(s[:i], s[i+1:]...)
// 求和
total := 0
for _, v := range nums {
total += v
}
// 切片排序(用标准库)
import "slices"
slices.Sort(nums) // 升序排序(Go 1.21+)
fmt.Println(slices.Contains(nums, 3)) // 是否包含某元素
fmt.Println(slices.Max(nums)) // 最大值
五、二维切片
grid := [][]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(grid[1][2]) // 6
新手常见坑
- 忘记接收 append 的返回值:必须
s = append(s, x) - 下标越界 panic:访问前确认
i < len(s) - 以为切片赋值是拷贝:切片赋值共享底层数组,修改互相影响
- 区间是左闭右开:
s[1:4]不包含下标 4 - nil 切片可以直接 append:
var s []int之后直接s = append(s, 1)是完全合法的,不需要先 make
练习
- 创建一个空的 int 切片,用循环 append 1~10,然后打印切片和它的长度。
- 给定
s := []int{5, 3, 8, 1, 9},找出最大值和最小值(先手写循环实现,再用slices.Max/Min验证)。 - 写一个函数
reverse(s []int) []int返回逆序的新切片。 - 运行下面代码,先猜输出再验证,解释原因:
a := []int{1, 2, 3}b := a[:2]b[0] = 99fmt.Println(a)
下一节:02 map 映射