Go 从 0 到精通 · 第 08 课:数组

学习定位:这是整套 Go 教程的第 8 课。
前置要求:已经完成第 7 课,掌握了指针的基本概念(&*nil、指针参数),理解了值传递与指针传递的区别。
本课目标:理解数组的定义、初始化、遍历方式,掌握数组是值类型的特性,为下一课学习切片建立对比基础。


1. 本课你要解决的核心问题

前面你用的变量一次只能存一个值。如果你想存一组数据——比如 5 个学生的成绩、7 天的气温——怎么办?

数组就是用来存固定数量同类型元素的数据结构。

你需要搞明白以下问题:

  • 数组怎么声明、怎么初始化
  • 数组的长度是类型的一部分意味着什么
  • 怎么访问和修改数组元素
  • 怎么遍历数组
  • 数组作为参数传递时会发生什么
  • 为什么 Go 中数组用得比切片少

学完这一课,你就能用数组存储一组数据了,同时也会理解为什么下一课的切片才是真正的主角。


2. 数组的基本概念

2.1 什么是数组

数组是一块连续的内存空间,存放固定数量的同类型元素。

你可以把数组想象成一排编号从 0 开始的储物柜:

1
2
索引:   [0]   [1]   [2]   [3]   [4]
值: 10 20 30 40 50
  • 每个格子叫一个元素
  • 每个元素有一个索引(从 0 开始)
  • 所有元素的类型必须相同
  • 数组的长度在创建时就确定了,不能再改

2.2 数组和之前学的变量的区别

普通变量 数组
存一个值 存多个值
x := 42 arr := [5]int{1,2,3,4,5}
用变量名访问 用下标访问 arr[0]

3. 数组的声明与初始化

3.1 声明并指定长度

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var arr [5]int // 声明一个长度为 5 的 int 数组

fmt.Println(arr) // [0 0 0 0 0]
fmt.Println(len(arr)) // 5
}

[5]int 表示"长度为 5 的 int 数组"。

没有显式赋值时,每个元素是类型的零值(int 的零值是 0)。

3.2 声明时初始化

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
arr := [5]int{10, 20, 30, 40, 50}

fmt.Println(arr) // [10 20 30 40 50]
}

花括号里的值按顺序赋给每个元素。

3.3 用 ... 让编译器推断长度

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
arr := [...]int{1, 2, 3, 4, 5}

fmt.Println(arr) // [1 2 3 4 5]
fmt.Println(len(arr)) // 5
}

[...]int 表示"你帮我数有多少个元素"。编译器会根据初始化值的数量确定长度。

3.4 按索引初始化

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
arr := [5]int{0: 10, 2: 30, 4: 50}

fmt.Println(arr) // [10 0 30 0 50]
}

0: 10 表示"下标 0 的元素是 10"。没有指定的元素保持零值。


4. 访问和修改数组元素

4.1 通过下标访问

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
arr := [5]int{10, 20, 30, 40, 50}

fmt.Println("第一个元素:", arr[0]) // 10
fmt.Println("第三个元素:", arr[2]) // 30
fmt.Println("最后一个:", arr[4]) // 50
}

arr[0] 表示"取下标为 0 的元素"。

4.2 通过下标修改

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
arr := [5]int{10, 20, 30, 40, 50}

arr[2] = 999
fmt.Println(arr) // [10 20 999 40 50]
}

4.3 下标越界

1
2
arr := [5]int{1, 2, 3, 4, 5}
fmt.Println(arr[5]) // 编译错误或运行时 panic

数组下标从 0 开始,长度为 5 的数组合法下标是 0 到 4。访问 arr[5] 会越界。

Go 在编译时如果能确定下标越界,会直接报编译错误。如果下标是变量,运行时会 panic。


5. 数组的遍历

5.1 用 for 经典循环遍历

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
arr := [5]int{10, 20, 30, 40, 50}

for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %d\n", i, arr[i])
}
}

输出:

1
2
3
4
5
arr[0] = 10
arr[1] = 20
arr[2] = 30
arr[3] = 40
arr[4] = 50

len(arr) 返回数组的长度。

5.2 用 for range 遍历(推荐)

1
2
3
4
5
6
7
8
9
10
11
package main

import "fmt"

func main() {
arr := [5]int{10, 20, 30, 40, 50}

for i, v := range arr {
fmt.Printf("arr[%d] = %d\n", i, v)
}
}

输出同上。

for range 每次迭代返回两个值:

  • i:当前元素的索引
  • v:当前元素的值(是拷贝)

5.3 只需要索引或只需要值

1
2
3
4
5
6
7
8
9
// 只需要索引
for i := range arr {
fmt.Println("索引:", i)
}

// 只需要值
for _, v := range arr {
fmt.Println("值:", v)
}

6. 数组是值类型:这是重点

6.1 赋值会复制整个数组

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
a := [3]int{1, 2, 3}
b := a // b 是 a 的完整副本

b[0] = 999

fmt.Println("a =", a) // [1 2 3]
fmt.Println("b =", b) // [999 2 3]
}

b = aa 的所有元素复制了一份给 b。修改 b 不影响 a

这和切片、map 不同——后面学到时你会看到,切片和 map 赋值后修改会影响原数据。

6.2 数组作为函数参数:也是值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func modify(arr [5]int) {
arr[0] = 999
fmt.Println("函数内部:", arr)
}

func main() {
a := [5]int{1, 2, 3, 4, 5}
modify(a)
fmt.Println("函数外部:", a)
}

输出:

1
2
函数内部: [999 2 3 4 5]
函数外部: [1 2 3 4 5]

数组传给函数时,整个数组被复制。函数内部修改的是副本。

6.3 用指针传递数组避免复制

如果不想复制整个数组,可以传指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func modify(arr *[5]int) {
arr[0] = 999 // Go 自动解引用,等价于 (*arr)[0]
}

func main() {
a := [5]int{1, 2, 3, 4, 5}
modify(&a)
fmt.Println("a =", a) // [999 2 3 4 5]
}

&a 就能把数组的地址传过去,函数内部直接操作原数组。

但请注意:[5]int[3]int 是不同的类型,*[5]int 不能传给期望 *[3]int 的函数。


7. 数组的长度是类型的一部分

这是 Go 数组最重要的特性之一:

1
2
var a [3]int
var b [5]int

[3]int[5]int完全不同的类型

这意味着:

  • 不能把 [3]int 赋给 [5]int
  • 不能把 [3]int 传给期望 [5]int 的函数
  • 数组长度必须在编译时确定
1
2
3
4
// 编译错误:类型不匹配
var a [3]int
var b [5]int
b = a // 不行

这种设计的好处是:编译器能在编译期发现长度不匹配的错误。但副作用是:数组不够灵活——你不能根据运行时的输入决定数组大小。这就是为什么切片(下一课)更常用。


8. 多维数组

8.1 二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var matrix [3][4]int // 3 行 4 列

matrix[0][0] = 1
matrix[1][2] = 5
matrix[2][3] = 9

for i, row := range matrix {
for j, val := range row {
fmt.Printf("matrix[%d][%d] = %d ", i, j, val)
}
fmt.Println()
}
}

[3][4]int 表示"3 个 [4]int",即 3 行每行 4 个元素。

8.2 初始化二维数组

1
2
3
4
matrix := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}

9. 一段综合示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import "fmt"

func main() {
// 声明并初始化
scores := [5]int{85, 92, 78, 95, 88}
fmt.Println("成绩:", scores)

// 计算平均分
total := 0
for _, s := range scores {
total += s
}
avg := float64(total) / float64(len(scores))
fmt.Printf("平均分:%.1f\n", avg)

// 找最高分
max := scores[0]
for _, s := range scores {
if s > max {
max = s
}
}
fmt.Println("最高分:", max)

// 数组赋值是复制
backup := scores
backup[0] = 100
fmt.Println("scores =", scores) // scores 没变
fmt.Println("backup =", backup)

// 用指针修改原数组
addBonus(&scores, 5)
fmt.Println("加分后:", scores)
}

func addBonus(arr *[5]int, bonus int) {
for i := range arr {
arr[i] += bonus
}
}

输出:

1
2
3
4
5
6
成绩: [85 92 78 95 88]
平均分:87.6
最高分: 95
scores = [85 92 78 95 88]
backup = [100 92 78 95 88]
加分后: [90 97 83 100 93]

10. 常见坑总结

10.1 以为数组赋值是引用

1
2
3
4
a := [3]int{1, 2, 3}
b := a
b[0] = 999
fmt.Println(a) // [1 2 3],不是 [999 2 3]

数组赋值是完整复制,不是共享引用。如果你想要"共享数据、修改互相影响"的行为,应该用切片。

10.2 忘记数组长度是类型的一部分

1
2
3
4
5
6
func process(arr [5]int) {}

func main() {
a := [3]int{1, 2, 3}
process(a) // 编译错误:[3]int 和 [5]int 类型不同
}

这导致数组作为函数参数非常不方便——每次改长度都要改函数签名。切片就没有这个问题。

10.3 数组长度必须是编译时常量

1
2
n := 5
var arr [n]int // 编译错误

Go 的数组长度必须是编译时能确定的常量。如果长度在运行时才知道,应该用切片。

10.4 for range 中的值是拷贝

1
2
3
4
5
arr := [3]int{1, 2, 3}
for _, v := range arr {
v = 999 // 修改的是 v,不影响 arr
}
fmt.Println(arr) // [1 2 3]

for range 返回的 v 是元素的副本,修改 v 不影响数组。要修改数组,用下标 arr[i]


11. 本课练习

练习 1:声明和初始化

要求:

  • 声明一个长度为 6 的 int 数组
  • 用字面量初始化为 {1, 2, 3, 4, 5, 6}
  • 打印数组和它的长度

练习 2:遍历求和

要求:

  • 定义一个包含若干个 float64 的数组
  • for range 遍历求和
  • 打印总和与平均值

练习 3:数组翻转

要求:

  • 定义一个 int 数组
  • 不使用额外数组,原地翻转(提示:头尾交换)
  • 打印翻转前后的数组

练习 4:验证值复制

要求:

  • 定义一个数组 a
  • a 赋给 b,修改 b 的某个元素
  • 打印 ab,验证 a 没有被影响

练习 5:用指针传递数组

要求:

  • 定义函数 zeroAll(arr *[5]int),将数组所有元素设为 0
  • main 中调用,验证原数组被修改

练习 6:二维数组

要求:

  • 定义一个 3x3 的二维数组
  • 初始化为单位矩阵(对角线为 1,其余为 0)
  • 打印这个矩阵

12. 自测题

12.1 概念题

  1. Go 数组的声明语法是什么?
  2. [3]int[5]int 是同一个类型吗?
  3. 数组赋值 b = a 是复制还是引用?
  4. 数组传给函数时是值传递还是引用传递?
  5. 怎么在函数里修改传入的数组?
  6. for range 遍历数组时,返回的值是拷贝还是引用?
  7. 数组长度能用变量指定吗?比如 var arr [n]intn 是变量)?
  8. 怎么用 ... 让编译器自动推断数组长度?

如果你能流畅回答这些问题,说明数组的基本概念你已经掌握了。


13. 本课总结

这一课你学到了 Go 中最基础的集合类型——数组。

你现在应该已经理解:

  • 数组用 [N]type 声明,长度固定,不可变
  • 通过下标 arr[i] 访问和修改元素,下标从 0 开始
  • 数组是值类型:赋值和传参都会复制整个数组
  • 要修改原数组,需要传指针 *[N]int
  • 数组长度是类型的一部分:[3]int[5]int 是不同类型
  • for range 遍历数组最方便
  • 数组在实际开发中用得不多,因为长度不可变且是类型的一部分,切片更灵活

14. 下一课预告

下一课我们学习 Go 最重要的数据结构之一:切片 slice

会重点讲:

  • 切片是什么,和数组有什么区别
  • 切片的创建方式
  • 长度 len 和容量 cap 的区别
  • append 怎么用,切片什么时候会扩容
  • 切片截取(slicing)和底层共享问题
  • 为什么切片才是 Go 日常开发中的主角

学完下一课,你就掌握了 Go 中处理"一组数据"的主力工具。