4.5 KiB
4.5 KiB
tags, create time
| tags | create time | |||||
|---|---|---|---|---|---|---|
|
2026-06-07 15:00 |
Go 语言数组与切片
概述
数组是固定长度的同类型序列,切片是动态增长的视图。本文对比两者的核心差异,重点讲解切片的 append、copy 和底层共享机制。
正文
数组 vs 切片:本质区别是什么?
数组 = 数据本身(值类型)
切片 = 指向底层数组的"窗口"(引用类型)
[!question] ❓ 思考 如果把数组传给函数,函数内修改元素会影响原数组吗?切片呢?
数组:长度是类型的一部分
var scores [3]int // [0 0 0],零值初始化
prices := [3]float64{10.99, 20.99, 30.99}
names := [...]string{"张三", "李四"} // [...] 让编译器自动计算长度
colors := [5]string{0: "红", 2: "蓝"} // 指定位置赋值 → [红 "" 蓝 "" ]
关键特性:[3]int 和 [5]int 是完全不同的类型,不能互相赋值。
数组是值类型——传给函数时会完整拷贝:
func sum(a [3]int) int {
total := 0
for _, v := range a {
total += v
}
return total
}
arr := [3]int{1, 2, 3}
fmt.Println(sum(arr)) // 6 — arr 被完整拷贝了一份
[!warning] ⚠️ 大数组传参陷阱 如果数组很大,拷贝代价很高。解决方案:传指针
[3]*int或改用切片。
切片:Go 最常用的集合类型
创建方式
// 直接字面量
fruits := []string{"苹果", "香蕉", "橙子"}
// make:指定 len 和可选 cap
nums := make([]int, 3, 5) // len=3, cap=5 → [0 0 0]
// 从数组/切片截取
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2 3 4] — 仍是数组的视图
[!note] 📝 切片的三字段结构 切片底层由三个字段组成:
切片 = {指针 → 底层数组起始位置, 长度(len), 容量(cap)}
len是可访问的元素个数,cap是从起始位置到数组末尾的空间。
graph LR
S["切片 s"] --> P["ptr: 指向 arr[1]"]
S --> L["len: 3"]
S --> C["cap: 4"]
P --> A["底层数组: [2 3 4 5 _]"]
append:动态扩容
shopping := []string{"牛奶"}
shopping = append(shopping, "面包") // ["牛奶", "面包"]
shopping = append(shopping, "水果", "蔬菜") // 一次追加多个
[!tip] 💡 append 的规则
- 如果剩余容量足够,直接在原数组上扩展
- 如果不够,Go 会分配一块更大的新数组(通常是 2x),把数据复制过去
- 返回值必须接收:
append可能返回新的切片头(因为底层数组换了)
copy:安全复制
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src) // dst=[1 2 3], n=3
[!warning] ⚠️ copy 只拷贝 min(lenSrc, lenDst) 个元素 如果目标比源短,只拷贝部分;如果目标更长,多余部分保持零值。
⚠️ 切片的共享陷阱
这是 Go 面试最高频的问题之一:
src := []int{1, 2, 3, 4, 5}
s1 := src[0:3] // [1 2 3], cap=5
s2 := s1[0:3] // [1 2 3], cap=5
s2[0] = 99 // 修改 s2
fmt.Println(s1) // [99 2 3] — s1 也被改了!
原因:s1 和 s2 共享同一个底层数组。修改一个会影响另一个。
[!warning] ⚠️ 截取时的容量问题 如果需要独立副本,务必用
copy()或重新创建:safe := make([]int, len(s1)) copy(safe, s1)
append 后的共享问题
a := make([]int, 3, 5) // len=3, cap=5
b := a[:3] // 共享底层数组
a = append(a, 99) // cap 够,不会重新分配
b[0] = 1 // 影响 a
fmt.Println(a) // [1 99 0 99] — 注意 cap=5 全变了
实战:遍历与处理
// 计算平均分
scores := []int{95, 89, 92, 88, 96}
sum := 0
for _, score := range scores {
sum += score
}
avg := float64(sum) / float64(len(scores))
fmt.Printf("平均分:%.2f\n", avg) // 92.00
何时用数组?何时用切片?
| 场景 | 推荐 |
|---|---|
| 长度固定且已知(如一周 7 天) | 数组 [7]int |
| 需要频繁增删元素 | 切片 []int |
| 函数参数(避免拷贝) | 切片 |
| 需要共享数据视图 | 切片截取 |
| 内存布局严格可控 | 数组 |
[!tip] 💡 实际开发经验 90% 的场景下你应该用切片。数组在性能敏感的低层代码或需要固定内存布局时有用武之地,但日常业务逻辑中切片更灵活、更安全。