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

8.5 KiB
Raw Permalink Blame History

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),包含 slicemapfunc 的数组不行。


📌 场景 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 代替
💡 适用于小而固定的集合 如坐标、颜色、哈希值等

推荐口诀: 小而固定用数组,大而可变用切片;传参小心复制坑,指针优化性能佳。