166 lines
4.5 KiB
Markdown
166 lines
4.5 KiB
Markdown
---
|
||
tags: [go, golang, go基础语法, 数组, 切片]
|
||
create time: 2026-06-07 15:00
|
||
---
|
||
|
||
# Go 语言数组与切片
|
||
|
||
## 概述
|
||
|
||
数组是固定长度的同类型序列,切片是动态增长的视图。本文对比两者的核心差异,重点讲解切片的 append、copy 和底层共享机制。
|
||
|
||
## 正文
|
||
|
||
### 数组 vs 切片:本质区别是什么?
|
||
|
||
```
|
||
数组 = 数据本身(值类型)
|
||
切片 = 指向底层数组的"窗口"(引用类型)
|
||
```
|
||
|
||
> [!question] ❓ 思考
|
||
> 如果把数组传给函数,函数内修改元素会影响原数组吗?切片呢?
|
||
|
||
### 数组:长度是类型的一部分
|
||
|
||
```go
|
||
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` 是**完全不同的类型**,不能互相赋值。
|
||
|
||
数组是**值类型**——传给函数时会完整拷贝:
|
||
|
||
```go
|
||
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 最常用的集合类型
|
||
|
||
#### 创建方式
|
||
|
||
```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` 是从起始位置到数组末尾的空间。
|
||
|
||
```mermaid
|
||
graph LR
|
||
S["切片 s"] --> P["ptr: 指向 arr[1]"]
|
||
S --> L["len: 3"]
|
||
S --> C["cap: 4"]
|
||
P --> A["底层数组: [2 3 4 5 _]"]
|
||
```
|
||
|
||
#### append:动态扩容
|
||
|
||
```go
|
||
shopping := []string{"牛奶"}
|
||
shopping = append(shopping, "面包") // ["牛奶", "面包"]
|
||
shopping = append(shopping, "水果", "蔬菜") // 一次追加多个
|
||
```
|
||
|
||
> [!tip] 💡 append 的规则
|
||
> - 如果剩余容量足够,直接在原数组上扩展
|
||
> - 如果不够,Go 会分配一块更大的新数组(通常是 2x),把数据复制过去
|
||
> - **返回值必须接收**:`append` 可能返回新的切片头(因为底层数组换了)
|
||
|
||
#### copy:安全复制
|
||
|
||
```go
|
||
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 面试最高频的问题之一:
|
||
|
||
```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()` 或重新创建:
|
||
> ```go
|
||
> safe := make([]int, len(s1))
|
||
> copy(safe, s1)
|
||
> ```
|
||
|
||
#### append 后的共享问题
|
||
|
||
```go
|
||
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 全变了
|
||
```
|
||
|
||
### 实战:遍历与处理
|
||
|
||
```go
|
||
// 计算平均分
|
||
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% 的场景下你应该用**切片**。数组在性能敏感的低层代码或需要固定内存布局时有用武之地,但日常业务逻辑中切片更灵活、更安全。
|