Go: Go 数组与切片
切片(slice)是 Go 处理集合数据的核心——它是对数组的轻量"view",动态扩容让集合操作既快又灵活。
数组在 Go 中很少直接使用(因为长度固定),但切片无处不在。理解切片必须先理解数组——切片的底层就是一个指向数组的指针 + 长度 + 容量。这节课你将掌握 Go 集合类型的全部核心。
1. 你将学到
- 数组的定义、initialize、iterate、比较
- 切片 4 种创建方式(字面量/array/make/零值)
len()与cap()的区别make的 3 个参数含义append自动扩容机制- 切片截取
[low:high:max] - 切片共享底层数组的坑(重点)
- 用切片实现动态数据采集器
2. 一个数据采集工程师的真实故事
(1) 痛点:固定数组装不下动态数据
Alice 是一个数据采集工程师,她最近需要写一个爬虫:
"我要从 5 个数据源各抓 10,000 条记录,存到内存里做去重。最开始我用 Python 列表,'append' 一下就完事。换成 Go 之后,我用
[10000]int这种定长数组,结果第一条记录就 panic:index out of range。"
她打开自己写的 Go 代码:
// 第一版:定长数组,无法扩展
var records [10000]int
for i := 0; i < 50000; i++ {
records[i] = i // ❌ i >= 10000 就 panic!
}
老板说:"数据源数量每天都在变,你这个定长数组没法用。"Alice 这才意识到:Go 的数组是值类型,长度是类型的一部分,[5]int 和 [10]int 是不同类型。
(2) Go 的解法:用切片(slice)
// data_collector.go
package main
import "fmt"
func main() {
// 切片:动态长度,自动扩容
var records []int // nil 切片,可以 append
// 模拟 5 个数据源各抓 10,000 条
for source := 1; source <= 5; source++ {
for i := 0; i < 10000; i++ {
value := source*10000 + i
records = append(records, value) // 自动扩容
}
}
fmt.Printf("总记录数:%d\n", len(records))
fmt.Printf("底层容量:%d\n", cap(records))
fmt.Printf("前 3 条:%v\n", records[:3])
fmt.Printf("后 3 条:%v\n", records[len(records)-3:])
}
输出:
总记录数:50000
底层容量:65536
前 3 条:[10000 10001 10002]
后 3 条:[59997 59998 59999]
(3) 收益:array vs 切片
| 维度 | array [n]T |
切片 []T |
|---|---|---|
| 长度 | 编译期固定 | 运行时可变 |
| 类型 | [5]int ≠ [10]int |
[]int 通用 |
| 传参 | 值传递(拷贝整个数组) | 引用传递(仅 24 字节) |
| 扩容 | 不支持 | append 自动 |
| 使用场景 | 固定大小(如 buffer) | 99% 场景 |
3. array
(1) 数组的定义与初始化
package main
import "fmt"
func main() {
// 写法 1:声明 + 初始化
var a1 [5]int = [5]int{1, 2, 3, 4, 5}
// 写法 2:类型推断
a2 := [5]int{1, 2, 3, 4, 5}
// 写法 3:部分初始化(其余为零值)
a3 := [5]int{1, 2} // [1 2 0 0 0]
// 写法 4:用 ... 让编译器推断长度
a4 := [...]int{1, 2, 3, 4} // 长度 4
// 写法 5:指定索引初始化
a5 := [5]int{0: 10, 4: 50} // [10 0 0 0 50]
fmt.Println(a1, a2, a3, a4, a5)
}
输出:
[1 2 3 4 5] [1 2 3 4 5] [1 2 0 0 0] [1 2 3 4] [10 0 0 0 50]
(2) 数组遍历
package main
import "fmt"
func main() {
nums := [4]string{"Alice", "Bob", "Charlie", "Dave"}
// 方式 1:for loop
for i := 0; i < len(nums); i++ {
fmt.Printf("%d: %s\n", i, nums[i])
}
// 方式 2:for range
for i, name := range nums {
fmt.Printf("[%d] %s\n", i, name)
}
}
(3) 数组是值类型
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999 // 修改的是副本
fmt.Printf("函数内:%v\n", arr)
}
func main() {
a := [3]int{1, 2, 3}
modify(a)
fmt.Printf("函数外:%v\n", a) // 原数组不变
}
输出:
函数内:[999 2 3]
函数外:[1 2 3]
▶ 示例:数组比较(仅限同类型同长度)
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}
fmt.Println(a == b) // true
fmt.Println(a == c) // false
// 不同长度或类型不可比较
// d := [4]int{1, 2, 3, 4}
// fmt.Println(a == d) // ❌ mismatched types
}
4. 切片 4 种创建方式
(1) 切片 = 数组的"视图"
graph TB
subgraph Slice["切片 slice(24 字节)"]
P["ptr<br/>指向底层数组"]
L["len<br/>长度"]
C["cap<br/>容量"]
end
subgraph Array["底层数组"]
A0["[0]"]
A1["[1]"]
A2["[2]"]
A3["[3]"]
A4["[4]"]
A5["[5]"]
end
P --> A1
P -.-> A2
P -.-> A3
切片是对底层数组的引用——只包含 24 字节(ptr + len + cap)。多个切片可以共享同一个底层数组。
(2) 创建方式 1:直接声明 nil 切片
var s []int // nil 切片,len=0, cap=0
// 可以 append,但不能用 s[0] 访问
(3) 创建方式 2:字面量
s := []int{1, 2, 3, 4, 5} // 创建并初始化
// 底层数组 = [1 2 3 4 5],len=cap=5
(4) 创建方式 3:make(指定 len 和 cap)
// 语法:make([]T, len, cap)
s := make([]int, 5) // len=5, cap=5, 全部为零值 [0 0 0 0 0]
s := make([]int, 3, 10) // len=3, cap=10
▶ 示例:make 4 种用法对比
package main
import "fmt"
func main() {
// 方式 1:nil 切片
var s1 []int
fmt.Printf("s1: len=%d cap=%d nil=%v\n", len(s1), cap(s1), s1 == nil)
// 方式 2:字面量
s2 := []int{1, 2, 3}
fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
// 方式 3:make 长度
s3 := make([]int, 5)
fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)
// 方式 4:make 长度 + 容量
s4 := make([]int, 3, 10)
fmt.Printf("s4: len=%d cap=%d %v\n", len(s4), cap(s4), s4)
// 方式 5:从数组切片
arr := [5]int{10, 20, 30, 40, 50}
s5 := arr[1:4] // [20 30 40],len=3 cap=4(剩余 4 个元素)
fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
}
输出:
s1: len=0 cap=0 nil=true
s2: len=3 cap=3 [1 2 3]
s3: len=5 cap=5 [0 0 0 0 0]
s4: len=3 cap=10 [0 0 0]
s5: len=3 cap=4 [20 30 40]
(5) 4 种创建方式速查
| 方式 | 语法 | 适用场景 |
|---|---|---|
| nil 切片 | var s []T |
延迟初始化 |
| 字面量 | []T{1, 2, 3} |
已知全部元素 |
| make | make([]T, len, cap) |
预分配容量 |
| 数组切片 | arr[low:high] |
子数组视图 |
5. len() 与 cap()
(1) len 和 cap 的关系
package main
import "fmt"
func main() {
s := make([]int, 3, 10) // len=3, cap=10
fmt.Printf("初始:len=%d cap=%d\n", len(s), cap(s))
// len=3:可访问 s[0], s[1], s[2]
// cap=10:底层数组有 10 个位置,还可 append 7 个
}
(2) cap 的意义
cap - len = 还能 append 多少元素不扩容。如果超过 cap,append 会分配新的底层数组(通常是 2 倍扩容)。
▶ 示例:len vs cap 的实际区别
package main
import "fmt"
func main() {
s := make([]int, 0, 5) // len=0, cap=5
for i := 1; i <= 8; i++ {
s = append(s, i)
fmt.Printf("append %d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
}
}
输出:
append 1: len=1 cap=5 [1]
append 2: len=2 cap=5 [1 2]
append 3: len=3 cap=5 [1 2 3]
append 4: len=4 cap=5 [1 2 3 4]
append 5: len=5 cap=5 [1 2 3 4 5]
append 6: len=6 cap=10 [1 2 3 4 5 6] ← cap 翻倍
append 7: len=7 cap=10 [1 2 3 4 5 6 7]
append 8: len=8 cap=10 [1 2 3 4 5 6 7 8]
len == cap 时,新 cap = 旧 cap * 2(小切片)或 旧 cap * 1.25(大切片,约 > 256)。
6. append 动态扩容
(1) append 基础
package main
import "fmt"
func main() {
var s []int // nil
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
// 批量 append:把一个 slice 拆开
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]
fmt.Println(s)
}
输出:
[1 2 3 4 5 6 7]
▶ 示例:append 删除元素(无内置 delete)
package main
import "fmt"
// 删除索引 i 处的元素
func removeAt(s []int, i int) []int {
// 拼接:s[:i] + s[i+1:]
return append(s[:i], s[i+1:]...)
}
func main() {
s := []int{1, 2, 3, 4, 5}
s = removeAt(s, 2) // 删除 3
fmt.Println(s) // [1 2 4 5]
}
输出:
[1 2 4 5]
(2) append 的陷阱:可能修改原数组
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
s2 := s[:3] // [1 2 3],共享底层数组
s2 = append(s2, 99) // 没扩容,修改了 s 的底层数组!
fmt.Println("s:", s) // [1 2 3 99 5]
fmt.Println("s2:", s2) // [1 2 3 99]
}
输出:
s: [1 2 3 99 5]
s2: [1 2 3 99]
copy() 或强制扩容。
7. 切片截取 [low:high:max]
(1) 三种切片表达式
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// 形式 1:[low:high] —— 取 [low, high)
s1 := arr[1:4]
fmt.Printf("arr[1:4] = %v, len=%d cap=%d\n", s1, len(s1), cap(s1))
// 形式 2:[low:] —— 从 low 到末尾
s2 := arr[2:]
fmt.Printf("arr[2:] = %v, len=%d cap=%d\n", s2, len(s2), cap(s2))
// 形式 3:[:high] —— 从头到 high
s3 := arr[:3]
fmt.Printf("arr[:3] = %v, len=%d cap=%d\n", s3, len(s3), cap(s3))
// 形式 4:[low:high:max] —— 限制 cap(防止意外追加影响原数组)
s4 := arr[1:3:4]
fmt.Printf("arr[1:3:4] = %v, len=%d cap=%d\n", s4, len(s4), cap(s4))
}
输出:
arr[1:4] = [20 30 40], len=3 cap=4
arr[2:] = [30 40 50], len=3 cap=3
arr[:3] = [10 20 30], len=3 cap=5
arr[1:3:4] = [20 30], len=2 cap=3
(2) [low:high:max] 的用途
max 限制 cap,防止 append 覆盖到原数组的其他部分:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
// 不限制 max:cap=4,append 后会覆盖 arr[3]
s1 := arr[1:3] // [20 30], cap=4
s1 = append(s1, 999)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← 被覆盖!
// 限制 max=3:cap=2,append 触发新数组
s2 := arr[1:3:3] // [20 30], cap=2
s2 = append(s2, 888)
fmt.Println("arr:", arr) // [10 20 30 999 50] ← 不变
fmt.Println("s2:", s2) // [20 30 888]
}
输出:
arr: [10 20 30 999 50]
arr: [10 20 30 999 50]
s2: [20 30 888]
8. 切片共享底层数组的坑(重点)
(1) 坑 1:修改切片元素影响所有切片
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3] // [1 2 3]
s2 := arr[2:5] // [3 4 5]
s1[2] = 999 // 同一个底层数组!
fmt.Println("s1:", s1) // [1 2 999]
fmt.Println("s2:", s2) // [999 4 5]
fmt.Println("arr:", arr) // [1 2 999 4 5]
}
(2) 坑 2:for range 时 append 导致死循环
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for _, v := range s {
s = append(s, v*10) // 危险:s 在变长
}
fmt.Println(s) // 输出不确定
}
for range 期间永远不要修改切片本身。Go 1.22 之前,for i, v := range s 中的 s 是循环开始时的长度快照;Go 1.22 修复了部分场景,但仍要小心。
(3) 坑 3:切片作为函数参数被意外修改
package main
import "fmt"
// 切片是引用传递,函数内的修改会影响外部
func addElement(s []int) {
s = append(s, 100) // 可能修改原数组(如果没扩容)
fmt.Printf("函数内: %v\n", s)
}
func main() {
s := make([]int, 3, 10)
s[0], s[1], s[2] = 1, 2, 3
addElement(s)
fmt.Printf("函数外: %v\n", s) // 取决于是否扩容
}
(4) 用 copy() 解决共享问题
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[0:3]
// copy 出一个独立副本
s2 := make([]int, len(s1))
copy(s2, s1)
s1[0] = 999
fmt.Println("s1:", s1) // [999 2 3]
fmt.Println("s2:", s2) // [1 2 3] ← 不受影响
}
▶ 示例:copy + append 实现切片插入
package main
import "fmt"
// 在索引 i 处插入元素 val
func insert(s []int, i int, val int) []int {
// 1. 扩容 1 个位置
s = append(s, 0)
// 2. 把 i 及之后的元素后移一位
copy(s[i+1:], s[i:])
// 3. 写入新元素
s[i] = val
return s
}
func main() {
s := []int{1, 2, 4, 5}
s = insert(s, 2, 3)
fmt.Println(s) // [1 2 3 4 5]
}
输出:
[1 2 3 4 5]
9. 完整示例:动态数据采集器
把切片的所有特性串起来,构建一个多源数据采集 + 去重 + 过滤的工具:
// collector.go
package main
import (
"fmt"
"strings"
)
// 数据源模拟函数
func fetchFromSource(sourceName string) []string {
data := map[string][]string{
"GitHub": {"repo:go", "repo:docker", "issue:bug", "repo:go"}, // 最后一个是重复
"NPM": {"pkg:react", "pkg:vue", "pkg:react"}, // 重复
"PyPI": {"pkg:requests", "pkg:flask"},
}
return data[sourceName]
}
// 去重:用 map 做哈希集合
func deduplicate(data []string) []string {
seen := make(map[string]bool)
result := make([]string, 0, len(data))
for _, item := range data {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// 过滤:只保留 repo: 开头的
func filterRepos(data []string) []string {
result := make([]string, 0)
for _, item := range data {
if strings.HasPrefix(item, "repo:") {
result = append(result, item)
}
}
return result
}
// Pipeline:组合多个步骤
func collectPipeline() (total, unique, repos int, sources []string) {
defer func() {
// 命名返回值 + defer 收集统计
fmt.Printf("\n[采集完成] total=%d unique=%d repos=%d sources=%v\n",
total, unique, repos, sources)
}()
allSources := []string{"GitHub", "NPM", "PyPI"}
var allData []string
for _, src := range allSources {
sources = append(sources, src)
data := fetchFromSource(src)
allData = append(allData, data...)
total += len(data)
}
uniqueData := deduplicate(allData)
unique = len(uniqueData)
repoData := filterRepos(uniqueData)
repos = len(repoData)
fmt.Println("\n=== 去重后的数据 ===")
for _, item := range uniqueData {
fmt.Printf(" %s\n", item)
}
fmt.Println("\n=== 过滤 repo 的数据 ===")
for _, item := range repoData {
fmt.Printf(" %s\n", item)
}
return total, unique, repos, sources
}
func main() {
collectPipeline()
}
预期输出:
=== 去重后的数据 ===
repo:go
repo:docker
issue:bug
pkg:react
pkg:vue
pkg:requests
pkg:flask
=== 过滤 repo 的数据 ===
repo:go
repo:docker
[采集完成] total=9 unique=7 repos=2 sources=[GitHub NPM PyPI]
make([]string, 0, len(data)) 预分配容量,避免 append 频繁扩容。这是性能优化的关键技巧。
❓ 常见问题
append 一定会扩容吗?len == cap 时才会扩容(分配新数组,2 倍复制)。如果还有容量空间,直接写入原数组。[low:high:max] 三参数的 max 什么时候必须用?nil 切片和空切片有什么区别?nil 切片(如 var s []int)== nil 为 true,len=0 cap=0;空切片(如 []int{} 或 make([]int, 0))== nil 为 false,len=0 cap=0。两者都能 append,但 JSON 序列化结果不同:nil → null,空 → []。list.remove(i)。Go 用 append(s[:i], s[i+1:]...) 实现删除(但不保留顺序)。如果要保留顺序,需要手动移动元素。copy(dst, src) — 复制 min(len(dst), len(src)) 个元素;(2) append([]T(nil), src...) — 复制全部;(3) 手动 for range。range 时性能差异?📖 小节
- 数组是值类型(定长、传参复制),切片是引用类型(动态、传参共享底层)
- 切片 4 种创建方式:nil / 字面量 / make / 数组切片
len是可见元素数,cap是底层数组容量,append在len==cap时扩容(2 倍策略)- 切片表达式
[low:high:max]三参数限制 cap,防止 append 越界 - 切片共享底层数组导致 3 个经典坑:修改元素互相影响 / range 时 append / 函数 append 副作用
- 解决方案:
copy()复制独立副本 / 强制扩容(append(s, 0)[:0])/ 限制 cap - 实际项目 99% 用切片,数组仅在固定大小或值语义场景
📝 作业
-
基础题(难度⭐):用切片实现一个
reverse(s []int) []int函数,把切片元素反转。提示:用 for 循环 + 临时变量,或原地交换。要求测试reverse([]int{1,2,3,4,5})→[5 4 3 2 1]。 -
进阶题(难度⭐⭐):实现一个
uniqueSorted(nums []int) []int函数:去重 + 升序排序。提示:先 sort.Ints 排序,再遍历去重。要求处理[]int{3, 1, 4, 1, 5, 9, 2, 6, 5}输出[1 2 3 4 5 6 9]。 -
挑战题(难度⭐⭐⭐):实现一个滑动窗口最大值算法:
maxSlidingWindow(nums []int, k int) []int。例如nums=[1,3,-1,-3,5,3,6,7], k=3→ 输出[3,3,5,5,6,7]。要求:(1) 时间复杂度 O(n);(2) 用deque(可用[]int模拟)维护窗口;(3) 解释为什么直接遍历会超时(O(n*k))。