在前面的文章中,我们已经学习了 Go 语言中的
break、continue和goto,掌握了如何控制循环执行流程。从这一篇开始,我们进入 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 中最常用的动态数据集合