Go 编程实战:数组 Array——固定长度的数据集合

0 阅读9分钟

在前面的文章中,我们已经学习了 Go 语言中的 breakcontinuegoto,掌握了如何控制循环执行流程。从这一篇开始,我们进入 Go 中非常重要的数据结构部分。

在实际开发中,我们经常需要保存一组相同类型的数据。例如保存学生成绩、服务器端口、商品价格、用户 ID 等。如果为每一个数据单独定义变量,不仅代码繁琐,而且不方便统一处理。

这时候就可以使用数组 Array。

数组是 Go 语言最基础的集合类型之一,也是后面学习 Slice、Map 等数据结构的重要基础。

一、什么是数组

数组是一组长度固定、类型相同的数据集合。

基本语法:

var 数组名 [长度]数据类型

例如:

var nums [5]int

表示定义一个长度为 5 的 int 数组。

可以理解为:

nums
+----+----+----+----+----+
| 0  | 0  | 0  | 0  | 0  |
+----+----+----+----+----+
  0    1    2    3    4

数组中的每个位置都有一个下标,下标从 0 开始。

长度为 5 的数组,其有效下标为:

0
1
2
3
4

因此最后一个元素的下标永远是:

len(nums) - 1

二、数组的零值

Go 语言变量在声明后即使没有手动赋值,也会自动获得对应类型的零值。

例如:

package main

import "fmt"

func main() {
	var nums [5]int

	fmt.Println(nums)
}

输出:

[0 0 0 0 0]

如果是字符串数组:

var names [3]string
fmt.Println(names)

结果中的每个元素都是空字符串。

布尔数组:

var flags [3]bool
fmt.Println(flags)

输出:

[false false false]

因此 Go 中定义数组后可以直接使用,不需要手动初始化所有元素。

三、给数组元素赋值

通过下标可以访问和修改数组中的元素:

package main

import "fmt"

func main() {
	var scores [5]int

	scores[0] = 90
	scores[1] = 85
	scores[2] = 96
	scores[3] = 78
	scores[4] = 88

	fmt.Println(scores)
}

输出:

[90 85 96 78 88]

也可以单独访问某个元素:

fmt.Println(scores[0])
fmt.Println(scores[2])

输出:

90
96

数组下标不能超过有效范围,例如:

var nums [5]int

nums[5] = 100

这里会出现越界问题,因为 [5]int 的有效下标只有 0~4

四、定义数组时直接初始化

除了先声明再赋值,还可以在创建数组的时候直接初始化。

nums := [5]int{10, 20, 30, 40, 50}

fmt.Println(nums)

输出:

[10 20 30 40 50]

字符串数组:

names := [3]string{"Tom", "Jack", "Lucy"}

如果没有提供所有元素:

nums := [5]int{10, 20}

实际上相当于:

[10 20 0 0 0]

剩余元素自动使用零值。

五、让 Go 自动计算数组长度

如果初始化数据比较多,可以使用:

nums := [...]int{10, 20, 30, 40, 50}

这里的:

...

表示让编译器自动计算数组长度。

因此:

nums := [...]int{10, 20, 30}

等价于:

nums := [3]int{10, 20, 30}

可以使用 len() 查看长度:

fmt.Println(len(nums))

输出:

3

这种写法在初始化数组时非常方便。

六、指定下标初始化

Go 还支持通过下标初始化数组。

例如:

nums := [5]int{
	0: 10,
	2: 30,
	4: 50,
}

fmt.Println(nums)

输出:

[10 0 30 0 50]

没有指定的位置仍然使用零值。

也可以写成:

nums := [...]int{
	2: 100,
	5: 200,
}

因为最大下标是 5,所以数组长度会自动推导为 6

七、遍历数组

数组最常见的操作之一就是遍历。

可以使用普通 for 循环:

package main

import "fmt"

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

	for i := 0; i < len(nums); i++ {
		fmt.Println(nums[i])
	}
}

这种方式适合需要使用下标的场景。

Go 更推荐使用 range

for index, value := range nums {
	fmt.Println(index, value)
}

输出:

0 10
1 20
2 30
3 40
4 50

其中:

index

表示当前下标,

value

表示当前元素。

如果只需要值:

for _, value := range nums {
	fmt.Println(value)
}

如果只需要下标:

for index := range nums {
	fmt.Println(index)
}

八、修改数组中的元素

数组中的元素是可以修改的:

nums := [3]int{10, 20, 30}

nums[1] = 100

fmt.Println(nums)

输出:

[10 100 30]

也可以通过循环修改:

for i := 0; i < len(nums); i++ {
	nums[i] *= 2
}

例如:

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

for i := 0; i < len(nums); i++ {
	nums[i] *= 2
}

fmt.Println(nums)

输出:

[2 4 6 8 10]

需要注意,如果这样写:

for _, value := range nums {
	value *= 2
}

原数组不会改变。

因为这里的 value 是数组元素值的副本,而不是原元素本身。

九、数组的长度属于类型的一部分

这是 Go 数组非常重要的特点。

下面两个变量:

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

虽然它们存储的都是 int,但是类型并不相同。

一个是:

[3]int

另一个是:

[5]int

因此不能直接:

a = b

这也是 Go 数组和很多其他语言数组之间比较明显的区别。

数组长度在编译阶段就已经确定,运行过程中不能改变。

十、数组之间可以比较

如果数组元素类型支持比较,那么相同类型的数组也可以直接使用:

==

和:

!=

例如:

a := [3]int{1, 2, 3}
b := [3]int{1, 2, 3}
c := [3]int{1, 2, 4}

fmt.Println(a == b)
fmt.Println(a == c)

输出:

true
false

只有数组长度、元素类型和每个位置的值都相同,两个数组才相等。

十一、数组是值类型

这是学习 Go 数组必须掌握的重点。

Go 中数组属于值类型

例如:

a := [3]int{10, 20, 30}

b := a

b[0] = 100

fmt.Println(a)
fmt.Println(b)

输出:

[10 20 30]
[100 20 30]

为什么修改 b 不影响 a

因为:

b := a

会复制整个数组。

此时可以理解为:

a -> [10 20 30]

b -> [10 20 30]

它们是两份独立的数据。

这与后面要学习的 Slice 有很大的区别。

十二、数组作为函数参数

因为数组是值类型,所以把数组传递给函数时,默认也是复制。

例如:

package main

import "fmt"

func change(nums [3]int) {
	nums[0] = 100
}

func main() {
	nums := [3]int{10, 20, 30}

	change(nums)

	fmt.Println(nums)
}

输出:

[10 20 30]

函数内部修改的是复制后的数组,因此不会影响原数组。

如果确实需要修改原数组,可以传递数组指针:

func change(nums *[3]int) {
	nums[0] = 100
}

调用:

change(&nums)

完整示例:

package main

import "fmt"

func change(nums *[3]int) {
	nums[0] = 100
}

func main() {
	nums := [3]int{10, 20, 30}

	change(&nums)

	fmt.Println(nums)
}

输出:

[100 20 30]

不过实际 Go 项目中,如果需要频繁向函数传递一组数据,通常不会直接传数组,而是使用下一篇要学习的 Slice。

十三、二维数组

数组中的元素本身也可以是数组。

例如:

var nums [3][4]int

表示:

3 行
4 列

可以理解为:

[
 [0 0 0 0]
 [0 0 0 0]
 [0 0 0 0]
]

初始化:

nums := [3][4]int{
	{1, 2, 3, 4},
	{5, 6, 7, 8},
	{9, 10, 11, 12},
}

访问第二行第三个元素:

fmt.Println(nums[1][2])

结果:

7

遍历二维数组:

for i := 0; i < len(nums); i++ {
	for j := 0; j < len(nums[i]); j++ {
		fmt.Printf("%d ", nums[i][j])
	}
	fmt.Println()
}

也可以使用 range

for _, row := range nums {
	for _, value := range row {
		fmt.Printf("%d ", value)
	}
	fmt.Println()
}

二维数组可以用于矩阵、棋盘、表格数据等场景。

十四、数组实战:计算成绩

例如保存 5 名学生的成绩:

package main

import "fmt"

func main() {
	scores := [5]int{86, 92, 78, 95, 88}

	sum := 0
	max := scores[0]
	min := scores[0]

	for _, score := range scores {
		sum += score

		if score > max {
			max = score
		}

		if score < min {
			min = score
		}
	}

	avg := float64(sum) / float64(len(scores))

	fmt.Println("总分:", sum)
	fmt.Println("平均分:", avg)
	fmt.Println("最高分:", max)
	fmt.Println("最低分:", min)
}

输出类似:

总分: 439
平均分: 87.8
最高分: 95
最低分: 78

通过这个例子可以看到,数组特别适合保存数量已经确定的一组相同类型的数据。

十五、数组适合什么场景

数组最大的特点就是:

长度固定

因此比较适合数量明确且不会变化的数据。

例如:

var week [7]string

表示一周七天。

var months [12]int

表示一年十二个月的数据。

或者:

rgb := [3]int{255, 128, 0}

表示 RGB 三个颜色通道。

但是现实项目中的数据数量通常是不固定的。

例如用户列表可能今天 100 个用户,明天 1000 个用户;商品列表可能不断增加;接口返回的数据数量也无法提前确定。

这时候固定长度数组就显得不够灵活。

十六、数组常见错误

第一个常见问题是数组越界:

nums := [3]int{1, 2, 3}

fmt.Println(nums[3])

数组只有:

0
1
2

三个有效下标,因此访问 3 会越界。

第二个问题是认为不同长度数组属于相同类型:

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

a = b

这是不允许的,因为 [3]int[5]int 是两个不同的类型。

第三个问题是误以为数组赋值共享数据:

a := [3]int{1, 2, 3}
b := a

b[0] = 100

这里不会修改 a,因为数组赋值会复制数据。

十七、数组与 Slice 的关系

学习数组之后,很快就会发现一个问题:

var nums [5]int

长度一旦确定就不能改变。

但是实际开发中的集合通常需要:

动态增加数据
动态删除数据
动态截取数据
向函数传递任意长度的数据

如果全部使用数组,实现起来会非常不方便。

因此 Go 提供了更加常用的数据结构:

Slice

也就是切片

切片底层和数组有非常密切的关系,但是相比数组更加灵活。

例如:

nums := []int{10, 20, 30}

注意这里没有数组长度:

[]int

它表示的就不再是数组,而是一个 int 类型的切片。

切片还可以动态追加元素:

nums = append(nums, 40)
nums = append(nums, 50)

这是数组无法直接做到的。

因此在实际 Go 项目中:

数组是基础
Slice 才是更常用的数据集合

理解数组的值类型、固定长度、下标访问以及内存结构之后,再学习 Slice 会容易很多。

总结

数组 Array 是 Go 语言中用于保存固定数量相同类型数据的基础数据结构。

定义数组:

var nums [5]int

初始化:

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

自动推导长度:

nums := [...]int{1, 2, 3}

访问元素:

nums[0]

获取长度:

len(nums)

遍历:

for index, value := range nums {
	fmt.Println(index, value)
}

需要重点记住三个特点:

1. 数组长度固定
2. 数组长度属于类型的一部分
3. 数组属于值类型,赋值和函数传参默认会复制

数组虽然在实际业务开发中的直接使用频率没有 Slice 高,但它是理解 Go 集合数据结构的重要基础。特别是 Slice 的底层实现与数组密切相关,因此数组这一部分一定要理解清楚。

下一篇:切片 Slice——Go 中最常用的动态数据集合