# 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` 代替 | | 💡 适用于小而固定的集合 | 如坐标、颜色、哈希值等 | > ✅ **推荐口诀**: > 小而固定用数组,大而可变用切片;传参小心复制坑,指针优化性能佳。