8.5 KiB
8.5 KiB
Go 语言数组详解:从基础到实践
在 Go 语言中,数组(Array) 是最基本、最底层的数据结构之一,虽然在实际开发中我们更多使用切片(Slice),但理解数组对于深入掌握 Go 的内存模型、值传递机制和性能优化至关重要。
1. 数组基础概念
数组是 Go 中一种固定长度、类型一致、有序存储的集合类型。
核心特征
- ✅ 类型一致性:所有元素必须是相同类型
- ✅ 固定长度:声明后长度不可变(编译期确定)
- ✅ 值类型:赋值和传参时会进行深拷贝
- ✅ 连续内存布局:元素在内存中连续存放,有利于缓存命中
⚠️ 注意:
[3]int和[5]int是两种完全不同的类型!
2. 数组声明与初始化
示例 1:基本声明与初始化
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 支持使用 … 让编译器自动推导数组长度。
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:指定索引初始化(稀疏数组风格)
可以跳过某些位置初始化,未指定的位置将使用零值填充。
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:复合类型的数组(结构体为例)
数组不仅能存基本类型,还能存储结构体、指针、数组等复合类型。
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() 返回值总是相等,因为容量 == 长度。
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!
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:遍历数组的三种方式
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:二维数组的声明与遍历
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 支持高维数组。
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:数组赋值产生副本
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:函数传参时的复制行为
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] — 未改变!
}
✅ 解决方案:若需修改原始数据,应传指针:
func modifyPtr(arr *[3]int) {
arr[0] = 999 // 等价于 (*arr)[0]
}
// 调用:modifyPtr(&original)
6. 数组的常见陷阱与注意事项
❌ 陷阱 1:不同长度的数组不能赋值
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),每次传参会复制整个数组,造成严重性能问题。
✅ 正确做法:使用指针。
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),它是对数组的抽象封装。
slice := []int{1, 2, 3}
slice = append(slice, 4) // ✅ 可以动态增长
7. 最佳实践与性能建议
| 实践 | 说明 |
|---|---|
| ✅ 优先使用切片 | 绝大多数场景下应使用 []int 而非 [5]int |
| ✅ 小数组可用值传递 | 若长度小(如 [3]float64 表示坐标),值传递更安全高效 |
| ✅ 大数组用指针传递 | 避免不必要的复制开销 |
| ✅ 利用连续内存优势 | 数组适合科学计算、图像处理等需缓存友好的场景 |
| ✅ 多维数组注意初始化格式 | 特别是混合维度时容易出错 |
8. 典型应用场景
📌 场景 1:固定配置参数
如 RGB 颜色表示:
var color [3]byte = [3]byte{255, 0, 128}
📌 场景 2:哈希表键(map key)
数组如果是可比较类型,可作为 map 的 key,而切片不能。
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:性能敏感的底层计算
如矩阵运算、密码学哈希中的定长缓冲区:
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 代替 |
| 💡 适用于小而固定的集合 | 如坐标、颜色、哈希值等 |
✅ 推荐口诀: 小而固定用数组,大而可变用切片;传参小心复制坑,指针优化性能佳。