Files
notes/resource/go/Go 语言的数组(Array).md
2026-03-01 01:43:46 +08:00

441 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Go 语言数组详解:从基础到实践
在 Go 语言中,**数组(Array)** 是最基本、最底层的数据结构之一,虽然在实际开发中我们更多使用切片(Slice),但理解数组对于深入掌握 Go 的内存模型、值传递机制和性能优化至关重要。
---
## 1. 数组基础概念
数组是 Go 中一种**固定长度、类型一致、有序存储**的集合类型。
### 核心特征
-**类型一致性**:所有元素必须是相同类型
-**固定长度**:声明后长度不可变(编译期确定)
-**值类型**:赋值和传参时会进行**深拷贝**
-**连续内存布局**:元素在内存中连续存放,有利于缓存命中
> ⚠️ 注意:`[3]int` 和 `[5]int` 是两种完全不同的类型!
---
## 2. 数组声明与初始化
### 示例 1:基本声明与初始化
```go
package main
import "fmt"
func main() {
var arr1 [5]int // 声明但未初始化,元素默认为零值
var arr2 = [5]int{1, 2, 3, 4, 5} // 显式初始化
arr3 := [5]int{10, 20, 30, 40, 50} // 简短声明
fmt.Println("arr1:", arr1) // [0 0 0 0 0]
fmt.Println("arr2:", arr2) // [1 2 3 4 5]
fmt.Println("arr3:", arr3) // [10 20 30 40 50]
}
```
---
### 示例 2:自动推导长度 `…`
Go 支持使用 `…` 让编译器自动推导数组长度。
```go
package main
import "fmt"
func main() {
arr := [...]int{1, 2, 3, 4, 5} // 自动推断为 [5]int
fmt.Println("数组长度:", len(arr)) // 输出:5
fmt.Println("数组内容:", arr) // [1 2 3 4 5]
// 注意:一旦推导完成,长度依然是固定的
}
```
---
### 示例 3:指定索引初始化(稀疏数组风格)
可以跳过某些位置初始化,未指定的位置将使用零值填充。
```go
package main
import "fmt"
func main() {
arr := [10]int{1: 100, 5: 500} // 第1和第5个元素被赋值
fmt.Println("指定索引初始化:", arr) // [0 100 0 0 0 500 0 0 0 0]
}
```
---
### 示例 4:复合类型的数组(结构体为例)
数组不仅能存基本类型,还能存储结构体、指针、数组等复合类型。
```go
package main
import "fmt"
type Student struct {
Name string
Age int
}
func main() {
students := [2]Student{
{"Alice", 20},
{"Bob", 22},
}
fmt.Println("学生数组:", students) // [{Alice 20} {Bob 22}]
}
```
---
## 3. 数组操作详解
### 示例 5:获取长度与容量
数组的 `len()``cap()` 返回值总是相等,因为容量 == 长度。
```go
package main
import "fmt"
func main() {
arr := [5]string{"a", "b", "c"}
fmt.Printf("长度: %d\n", len(arr)) // 5
fmt.Printf("容量: %d\n", cap(arr)) // 5
}
```
---
### 示例 6:安全访问与修改元素
越界访问会导致 panic
```go
package main
import "fmt"
func main() {
arr := [3]int{10, 20, 30}
// 访问
fmt.Println("第一个元素:", arr[0]) // 10
// 修改
arr[1] = 99
fmt.Println("修改后:", arr) // [10 99 30]
// ❌ 错误示例(会 panic)—— 超出范围
// arr[5] = 100 // panic: runtime error: index out of range
}
```
---
### 示例 7:遍历数组的三种方式
```go
package main
import "fmt"
func main() {
arr := [4]int{1, 3, 5, 7}
fmt.Println("1. 使用索引遍历:")
for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
fmt.Println("\n2. 使用 range 索引:")
for i := range arr {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
fmt.Println("\n3. 使用 range 获取索引和值:")
for idx, val := range arr {
fmt.Printf("索引 %d => 值 %d\n", idx, val)
}
fmt.Println("\n4. 忽略索引,只获取值:")
for _, val := range arr {
fmt.Printf("值: %d ", val)
}
fmt.Println()
}
```
---
## 4. 多维数组详解
### 示例 8:二维数组的声明与遍历
```go
package main
import "fmt"
func main() {
// 声明一个 3x4 的二维数组
matrix := [3][4]int{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11},
}
fmt.Println("二维数组内容:")
for i := 0; i < len(matrix); i++ {
for j := 0; j < len(matrix[i]); j++ {
fmt.Printf("%4d", matrix[i][j])
}
fmt.Println()
}
}
```
> 输出:
```
0 1 2 3
4 5 6 7
8 9 10 11
```
---
### 示例 9:三维及以上数组(了解)
虽然不常用,但 Go 支持高维数组。
```go
package main
import "fmt"
func main() {
cube := [2][3][2]int{
{{1, 2}, {3, 4}, {5, 6}},
{{7, 8}, {9, 10}, {11, 12}},
}
fmt.Println("三维数组 [0][1][1]:", cube[0][1][1]) // 输出:4
}
```
---
## 5. 数组作为值类型的特性
这是 Go 数组最重要的特性之一:**值类型 vs 引用类型**
### 示例 10:数组赋值产生副本
```go
package main
import "fmt"
func main() {
arr1 := [4]int{1, 2, 3, 4}
arr2 := arr1 // 完整复制一份(深拷贝)
arr2[0] = 999
fmt.Println("arr1:", arr1) // [1 2 3 4] — 不受影响
fmt.Println("arr2:", arr2) // [999 2 3 4]
}
```
---
### 示例 11:函数传参时的复制行为
```go
package main
import "fmt"
func modify(arr [3]int) {
arr[0] = 999
fmt.Println("函数内 arr:", arr) // [999 2 3]
}
func main() {
original := [3]int{1, 2, 3}
modify(original)
fmt.Println("函数外 original:", original) // [1 2 3] — 未改变!
}
```
> ✅ 解决方案:若需修改原始数据,应传指针:
```go
func modifyPtr(arr *[3]int) {
arr[0] = 999 // 等价于 (*arr)[0]
}
// 调用:modifyPtr(&original)
```
---
## 6. 数组的常见陷阱与注意事项
### ❌ 陷阱 1:不同长度的数组不能赋值
```go
package main
func main() {
// var a [3]int
// var b [4]int
// a = b // 编译错误:cannot use b (type [4]int) as type [3]int
}
```
虽然都是整型数组,但 `[3]int``[4]int`,属于不同类型。
---
### ❌ 陷阱 2:大型数组传递性能差
如果数组很大(如 `[10000]int`),每次传参会复制整个数组,造成严重性能问题。
✅ 正确做法:使用指针。
```go
package main
import "fmt"
func process(data *[1000]int) {
// 直接操作原数据,无复制开销
data[0] = 100
}
func main() {
largeArr := [1000]int{}
process(&largeArr)
fmt.Println("largeArr[0]:", largeArr[0]) // 100
}
```
---
### ❌ 陷阱 3:无法动态扩容
数组长度固定,不能追加元素。
✅ 替代方案:使用切片(slice),它是对数组的抽象封装。
```go
slice := []int{1, 2, 3}
slice = append(slice, 4) // ✅ 可以动态增长
```
---
## 7. 最佳实践与性能建议
| 实践 | 说明 |
|------|------|
| ✅ 优先使用切片 | 绝大多数场景下应使用 `[]int` 而非 `[5]int` |
| ✅ 小数组可用值传递 | 若长度小(如 `[3]float64` 表示坐标),值传递更安全高效 |
| ✅ 大数组用指针传递 | 避免不必要的复制开销 |
| ✅ 利用连续内存优势 | 数组适合科学计算、图像处理等需缓存友好的场景 |
| ✅ 多维数组注意初始化格式 | 特别是混合维度时容易出错 |
---
## 8. 典型应用场景
### 📌 场景 1:固定配置参数
如 RGB 颜色表示:
```go
var color [3]byte = [3]byte{255, 0, 128}
```
### 📌 场景 2:哈希表键(map key)
数组如果是可比较类型,可作为 map 的 key,而切片不能。
```go
package main
import "fmt"
func main() {
// 使用 [2]int 作为 map 键
coordMap := make(map[[2]int]string)
coordMap[[2]int{0, 0}] = "origin"
coordMap[[2]int{3, 4}] = "point A"
fmt.Println(coordMap) // map[[0 0]:origin [3 4]:point A]
}
```
> ✅ 注意:只有**可比较类型**的数组才能做 key(如 `[2]int`),包含 `slice`、`map`、`func` 的数组不行。
---
### 📌 场景 3:性能敏感的底层计算
如矩阵运算、密码学哈希中的定长缓冲区:
```go
package main
import (
"crypto/sha256"
"fmt"
)
func main() {
data := []byte("hello")
hash := sha256.Sum256(data) // 返回 [32]byte 类型
fmt.Printf("SHA256: %x\n", hash)
fmt.Printf("类型是: %T\n", hash) // [32]uint8
}
```
---
## 总结
| 关键点 | 说明 |
|--------|------|
| 🧩 数组是 Go 的基础结构 | 了解它是理解切片的前提 |
| 🔐 值类型带来安全性 | 避免意外修改,但也带来开销 |
| ⚡ 连续内存提升性能 | 适合高性能计算场景 |
| 🚫 无法扩容 | 日常开发建议用 `slice` 代替 |
| 💡 适用于小而固定的集合 | 如坐标、颜色、哈希值等 |
> ✅ **推荐口诀**
> 小而固定用数组,大而可变用切片;传参小心复制坑,指针优化性能佳。