vault backup: 2026-06-07 12:14:39
This commit is contained in:
+129
-105
@@ -1,141 +1,165 @@
|
||||
---
|
||||
tags:
|
||||
- Go
|
||||
- golang
|
||||
- go基础语法
|
||||
- 数组
|
||||
- 切片
|
||||
tags: [go, golang, go基础语法, 数组, 切片]
|
||||
create time: 2026-06-07 15:00
|
||||
---
|
||||
|
||||
# Go语言数组与切片
|
||||
# Go 语言数组与切片
|
||||
|
||||
在Go语言中,数组和切片是两种重要的数据结构,它们看起来很相似,但使用场景和特点却大不相同。今天就让我们通过一些生动的例子,深入理解这两个概念。
|
||||
## 概述
|
||||
|
||||
## 数组:固定长度的"储物柜"
|
||||
数组是固定长度的同类型序列,切片是动态增长的视图。本文对比两者的核心差异,重点讲解切片的 append、copy 和底层共享机制。
|
||||
|
||||
### 什么是数组?
|
||||
想象一下,数组就像一个固定大小的储物柜,有着明确数量的格子,每个格子只能存放相同类型的物品。一旦这个储物柜建好了,格子的数量就不能改变了。
|
||||
## 正文
|
||||
|
||||
### 如何定义数组
|
||||
在Go语言中,定义数组有多种方式:
|
||||
### 数组 vs 切片:本质区别是什么?
|
||||
|
||||
```go
|
||||
// 1. 最基础的定义方式
|
||||
var scores [3]int // 定义一个能存放3个整数的数组
|
||||
|
||||
// 2. 定义时直接赋值
|
||||
var prices = [3]float64{10.99, 20.99, 30.99}
|
||||
|
||||
// 3. 让编译器自动计算长度
|
||||
names := [...]string{"张三", "李四", "王五"}
|
||||
|
||||
// 4. 指定特定位置的值
|
||||
colors := [5]string{0: "红", 2: "蓝", 4: "绿"} // [红 "" 蓝 "" 绿]
|
||||
```
|
||||
数组 = 数据本身(值类型)
|
||||
切片 = 指向底层数组的"窗口"(引用类型)
|
||||
```
|
||||
|
||||
### 数组的特点
|
||||
1. 长度固定:一旦定义,长度不可改变
|
||||
2. 类型固定:同一个数组只能存储相同类型的元素
|
||||
3. 长度是类型的一部分:`[3]int`和`[5]int`是不同的类型
|
||||
> [!question] ❓ 思考
|
||||
> 如果把数组传给函数,函数内修改元素会影响原数组吗?切片呢?
|
||||
|
||||
### 数组:长度是类型的一部分
|
||||
|
||||
### 实战小例子
|
||||
```go
|
||||
package main
|
||||
var scores [3]int // [0 0 0],零值初始化
|
||||
prices := [3]float64{10.99, 20.99, 30.99}
|
||||
names := [...]string{"张三", "李四"} // [...] 让编译器自动计算长度
|
||||
colors := [5]string{0: "红", 2: "蓝"} // 指定位置赋值 → [红 "" 蓝 "" ]
|
||||
```
|
||||
|
||||
import "fmt"
|
||||
**关键特性**:`[3]int` 和 `[5]int` 是**完全不同的类型**,不能互相赋值。
|
||||
|
||||
func main() {
|
||||
// 成绩管理系统
|
||||
var scores [5]int = [5]int{95, 89, 92, 88, 96}
|
||||
|
||||
// 计算平均分
|
||||
sum := 0
|
||||
for _, score := range scores {
|
||||
sum += score
|
||||
数组是**值类型**——传给函数时会完整拷贝:
|
||||
|
||||
```go
|
||||
func sum(a [3]int) int {
|
||||
total := 0
|
||||
for _, v := range a {
|
||||
total += v
|
||||
}
|
||||
average := float64(sum) / float64(len(scores))
|
||||
fmt.Printf("平均分:%.2f\n", average)
|
||||
return total
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
平均分:92.00
|
||||
|
||||
arr := [3]int{1, 2, 3}
|
||||
fmt.Println(sum(arr)) // 6 — arr 被完整拷贝了一份
|
||||
```
|
||||
|
||||
## 切片:灵活的"动态储物柜"
|
||||
> [!warning] ⚠️ 大数组传参陷阱
|
||||
> 如果数组很大,拷贝代价很高。解决方案:传指针 `[3]*int` 或改用切片。
|
||||
|
||||
### 为什么需要切片?
|
||||
如果说数组是固定大小的储物柜,那么切片就像是一个可以随时扩展的"魔法储物柜"。它的大小是可以动态调整的,这让我们在处理不确定数量的数据时更加灵活。
|
||||
### 切片:Go 最常用的集合类型
|
||||
|
||||
#### 创建方式
|
||||
|
||||
### 切片的创建方式
|
||||
```go
|
||||
// 1. 直接创建
|
||||
// 直接字面量
|
||||
fruits := []string{"苹果", "香蕉", "橙子"}
|
||||
|
||||
// 2. 使用make函数创建
|
||||
numbers := make([]int, 3, 5) // 长度为3,容量为5的切片
|
||||
// make:指定 len 和可选 cap
|
||||
nums := make([]int, 3, 5) // len=3, cap=5 → [0 0 0]
|
||||
|
||||
// 3. 从数组创建
|
||||
// 从数组/切片截取
|
||||
arr := [5]int{1, 2, 3, 4, 5}
|
||||
slice := arr[1:4] // [2 3 4]
|
||||
s := arr[1:4] // [2 3 4] — 仍是数组的视图
|
||||
```
|
||||
|
||||
### 切片的三个核心概念
|
||||
1. 指针:指向底层数组的第一个可见元素
|
||||
2. 长度:切片当前的元素个数(len)
|
||||
3. 容量:从切片起始位置到底层数组末尾的元素个数(cap)
|
||||
> [!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
|
||||
package main
|
||||
shopping := []string{"牛奶"}
|
||||
shopping = append(shopping, "面包") // ["牛奶", "面包"]
|
||||
shopping = append(shopping, "水果", "蔬菜") // 一次追加多个
|
||||
```
|
||||
|
||||
import "fmt"
|
||||
> [!tip] 💡 append 的规则
|
||||
> - 如果剩余容量足够,直接在原数组上扩展
|
||||
> - 如果不够,Go 会分配一块更大的新数组(通常是 2x),把数据复制过去
|
||||
> - **返回值必须接收**:`append` 可能返回新的切片头(因为底层数组换了)
|
||||
|
||||
func main() {
|
||||
// 创建一个购物清单
|
||||
shoppingList := []string{"牛奶"}
|
||||
|
||||
// 添加商品
|
||||
shoppingList = append(shoppingList, "面包")
|
||||
shoppingList = append(shoppingList, "水果", "蔬菜")
|
||||
|
||||
fmt.Printf("购物清单:%v\n", shoppingList)
|
||||
fmt.Printf("清单长度:%d\n", len(shoppingList))
|
||||
fmt.Printf("清单容量:%d\n", cap(shoppingList))
|
||||
|
||||
// 复制切片
|
||||
backup := make([]string, len(shoppingList))
|
||||
copy(backup, shoppingList)
|
||||
|
||||
fmt.Printf("备份清单:%v\n", backup)
|
||||
#### 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
|
||||
}
|
||||
```
|
||||
运行结果:
|
||||
```
|
||||
购物清单:[牛奶 面包 水果 蔬菜]
|
||||
清单长度:4
|
||||
清单容量:4
|
||||
备份清单:[牛奶 面包 水果 蔬菜]
|
||||
avg := float64(sum) / float64(len(scores))
|
||||
fmt.Printf("平均分:%.2f\n", avg) // 92.00
|
||||
```
|
||||
|
||||
### 切片的注意事项
|
||||
1. 切片是引用类型,多个切片可能共享同一个底层数组
|
||||
2. append可能导致重新分配内存,生成新的底层数组
|
||||
3. 使用make创建切片时,可以指定容量来减少内存重新分配的次数
|
||||
### 何时用数组?何时用切片?
|
||||
|
||||
## 实际应用场景对比
|
||||
| 场景 | 推荐 |
|
||||
|------|------|
|
||||
| 长度固定且已知(如一周 7 天) | 数组 `[7]int` |
|
||||
| 需要频繁增删元素 | 切片 `[]int` |
|
||||
| 函数参数(避免拷贝) | 切片 |
|
||||
| 需要共享数据视图 | 切片截取 |
|
||||
| 内存布局严格可控 | 数组 |
|
||||
|
||||
1. 什么时候用数组?
|
||||
- 当你确切知道数据的长度,且不会改变时
|
||||
- 例如:存储一周七天的温度数据
|
||||
|
||||
2. 什么时候用切片?
|
||||
- 处理动态数据,如用户输入的列表
|
||||
- 需要对数据进行频繁的添加和删除操作
|
||||
- 作为函数参数传递(更加灵活)
|
||||
|
||||
## 小结
|
||||
|
||||
数组和切片各有特点:数组像是一个固定大小的储物柜,适合存放固定数量的元素;而切片则像是一个可伸缩的储物空间,更适合处理动态数据。在实际开发中,我们更多地使用切片,因为它提供了更好的灵活性和便利性。
|
||||
|
||||
记住:选择数组还是切片,关键要看具体的使用场景。理解它们的特点,才能在编程时做出正确的选择。
|
||||
> [!tip] 💡 实际开发经验
|
||||
> 90% 的场景下你应该用**切片**。数组在性能敏感的低层代码或需要固定内存布局时有用武之地,但日常业务逻辑中切片更灵活、更安全。
|
||||
|
||||
Reference in New Issue
Block a user