跳到主要内容

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

数组的两个重要特性(也是它不常用的原因):

  1. 长度是类型的一部分[3]int[4]int 是两种不同的类型,不能互相赋值
  2. 赋值和传参会复制整个数组
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 超过容量时会分配新的底层数组。新手阶段先记住两条实用结论:

  1. 切片赋值共享数据,要独立副本用 copy
  2. append 的结果必须赋值回原变量

四、常用操作模式

// 判断切片是否为空
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

新手常见坑

  1. 忘记接收 append 的返回值:必须 s = append(s, x)
  2. 下标越界 panic:访问前确认 i < len(s)
  3. 以为切片赋值是拷贝:切片赋值共享底层数组,修改互相影响
  4. 区间是左闭右开s[1:4] 不包含下标 4
  5. nil 切片可以直接 appendvar s []int 之后直接 s = append(s, 1) 是完全合法的,不需要先 make

练习

  1. 创建一个空的 int 切片,用循环 append 1~10,然后打印切片和它的长度。
  2. 给定 s := []int{5, 3, 8, 1, 9},找出最大值和最小值(先手写循环实现,再用 slices.Max/Min 验证)。
  3. 写一个函数 reverse(s []int) []int 返回逆序的新切片。
  4. 运行下面代码,先猜输出再验证,解释原因:
    a := []int{1, 2, 3}
    b := a[:2]
    b[0] = 99
    fmt.Println(a)

下一节:02 map 映射