本章学习目标
- 学会声明和初始化数组
- 学会赋值和访问数组中的元素
- 学会迭代数组
数组是一种定长且有序的元素收集器。本章将使用数组存储太阳系中行星和矮行星的名字,但除此之外,数组也可以用于存储其他任何事物。
请考虑这一点
你现在或者以前收藏过邮票、硬币、贴纸、书籍、鞋子、奖杯、电影之类的东西吗?
数组能够收集大量同类型的事物,你觉得它能够用来表示何种收藏集合?
16.1 声明数组并访问其元素
以下planets
数组不多不少正好包含8个元素:
var planets [8]string
同一数组中的每个元素都具有相同的类型,例如,这个例子中的planets
数组就由8个字符串组成,简称字符串数组。
正如图16-1和代码清单16-1所示,数组中的每个元素都可以通过方括号[]
和一个以0为起始的索引进行访问。
图16-1 被索引0至7标记的行星
代码清单16-1 存储行星的数组:array.go
var planets [8]string
planets[0] = "Mercury" ←--- 将行星赋值给索引0
planets[1] = "Venus"
planets[2] = "Earth"
earth := planets[2] ←--- 获取索引2存储的行星
fmt.Println(earth) ←--- 打印出“Earth”
数组的长度可以通过内置的len
函数确定。在声明数组时,未被赋值的元素将包含类型对应的零值。例如,虽然上面的代码只赋值了3个元素,但planets
数组仍将包含8个元素,其中未被赋值的5个元素将被初始化为string
类型的零值,也就是空字符串:
fmt.Println(len(planets)) ←--- 打印出“8”
fmt.Println(planets[3] == "") ←--- 打印出“true”
注意 Go具有少量不需要使用
import
语句载入即可使用的内置函数,其中的len
函数可以用于确定多种不同类型的长度。例如,在上面的示例中,它返回的就是数组的长度。
速查16-1
1.怎样才能访问planets
数组的第一个元素?
2.对新创建的整数数组来说,元素的默认值是什么?
16.2 小心越界
包含8个元素的数组的合法索引为0至7。Go编译器在检测到对越界元素的访问时会报错:
var planets [8]string
planets[8] = "Pluto" ←--- 无效的数组索引8(越过了8元素数组的边界)
pluto := planets[8]
另外,如果Go编译器在编译时未能发现越界错误,那么程序将在运行时出现惊恐(panic):
var planets [8]string
i := 8
planets[i] = "Pluto" ←--- 惊恐:运行时错误:索引越界
pluto := planets[i]
惊恐会导致程序崩溃,但这总比像C语言那样修改了不属于planets
数组的内存而导致未明确行为好。
速查16-2
访问planets[11]
会导致编译时错误还是运行时惊恐?
16.3 使用复合字面量初始化数组
复合字面量是一种使用给定值对任意复合类型实施初始化的紧凑语法。与先声明一个数组然后再一个接一个地为它的元素赋值相比,Go语言的复合字面量语法允许我们在单个步骤里面完成声明数组和初始化数组这两项工作,就像代码清单16-2所示的那样。
代码清单16-2 矮行星数组:dwarfs.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
这段代码中的大括号{}
包含了5个用逗号分隔的字符串,它们将被用于填充新创建的数组。
在初始化大型数组时,将复合字面量拆分至多个行可以让代码变得更可读。为了方便,你还可以在复合字面量里面使用省略号…
而不是具体的数字作为数组长度,然后让Go编译器为你计算数组元素的数量。需要注意的是,无论使用哪种方式初始化数组,数组的长度都是固定的。
速查16-3
请使用Go内置的len函数获取代码清单16-3中定义的planets
数组的长度。
代码清单16-3 完整的行星数组:composite.go
planets := [...]string{ ←--- 让Go编译器计算数组元素的数量
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune", ←--- 结尾的逗号是必需的,不能省略
}
16.4 迭代数组
正如代码清单16-4所示,迭代数组中各个元素的做法与第9章中迭代字符串中各个字符的做法非常相似。
代码清单16-4 遍历数组:array-loop.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i := 0; i < len(dwarfs); i++ {
dwarf := dwarfs[i]
fmt.Println(i, dwarf)
}
正如代码清单16-5所示,使用关键字range
可以取得数组中每个元素对应的索引和值,这种迭代方式使用的代码更少并且更不容易出错。
代码清单16-5 使用关键字range
迭代数组:array-range.go
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
for i, dwarf := range dwarfs {
fmt.Println(i, dwarf)
}
代码清单16-4和代码清单16-5将产生相同的输出:
0 Ceres
1 Pluto
2 Haumea
3 Makemake
4 Eris
注意 正如之前所述,如果你不需要
range
提供的索引变量,那么可以使用空白标识符(下划线)来省略它们。
速查16-4
1.使用关键字range
迭代数组可以避免哪些错误?
2.在什么情况下,使用传统的for
循环比使用关键字range
更适合?
16.5 数组被复制
正如代码清单16-6所示,无论是将数组赋值给新的变量还是将它传递给函数,都会产生一个完整的数组副本。
代码清单16-6 数组是一个值:array-value.go
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
planetsMarkII := planets ←--- 复制planets数组
planets[2] = "whoops" ←--- 修改数组元素,让出星际轨道
fmt.Println(planets) ←--- 打印出“[Mercury Venus whoops Mars Jupiter Saturn Uranus Neptune]”
fmt.Println(planetsMarkII) ←--- 打印出“[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]”
提示 天有不测之风云,如果有一天地球毁灭而你需要离开地球的时候,你肯定希望计算机上能够有一个Go编译器。请按照Go官方网站给出的指令安装Go编译器。
因为数组也是一种值,而函数通过传递值接受参数,所以代码清单16-7中的terraform
函数将非常低效。
代码清单16-7 数组作为值进行传递:terraform.go
package main
import "fmt"
// terraform函数不会产生任何实际效果
func terraform(planets [8]string) {
for i := range planets {
planets[i] = "New " + planets[i]
}
}
func main() {
planets := [...]string{
"Mercury",
"Venus",
"Earth",
"Mars",
"Jupiter",
"Saturn",
"Uranus",
"Neptune",
}
terraform(planets)
fmt.Println(planets) ←--- 打印出“[Mercury Venus Earth Mars Jupiter Saturn Uranus Neptune]”
}
由于terraform
函数操作的是planets
数组的副本,因此函数内部对数组的修改将不会影响main
函数中的planets
数组。
除此之外,我们还需要意识到数组的长度实际上也是数组类型的一部分,这一点非常重要。例如,虽然[8]string
类型和[5]string
类型都属于字符串收集器,但它们实际上是不同的类型。尝试传递长度不相符的数组作为参数将导致Go编译器报错:
dwarfs := [5]string{"Ceres", "Pluto", "Haumea", "Makemake", "Eris"}
terraform(dwarfs) ←--- 只能接受 [8]string类型的terraform函数无法使用[5]string类型的dwarfs作为实参
基于上述原因,函数一般使用切片而不是数组作为形参,接下来的第17章将对切片进行介绍。
速查16-5
1.在代码清单16-6中,
planetsMarkII
数组的Earth
元素为何没有被改变?
2.我们应如何修改代码清单16-7,才能够让
main
函数中的planets
数组发生改变?
16.6 由数组组成的数组
我们除可以定义字符串数组之外,还可以定义整数数组、浮点数数组甚至数组的数组。例如,代码清单16-8就展示了如何创建一个由字符串数组组成的数组,并将其用于表示8 × 8国际象棋棋盘。
代码清单16-8 国际象棋棋盘:chess.go
var board [8][8]string ←--- 一个8×8嵌套数组,其中内层数组的每个元素都是一个字符串
board[0][0] = "r"
board[0][7] = "r" ←--- 将“车”(rook)放置到[行][列]指定的坐标上
for column := range board[1] {
board[1][column] = "p"
}
fmt.Print(board)
速查16-6
如果我们要设计一个数独游戏程序,那么应该如何声明9 × 9的整数网格?
16.7 小结
- 数组是一种定长且有序的元素收集器。
- 复合字面量能够为数组初始化提供方便。
- 关键字
range
可以用于迭代数组。 - 在访问数组元素时,数组的索引必须位于边界范围之内。
- 数组在被赋值或者被传递至函数的时候,都会产生相应的副本。
为了检验你是否已经掌握了上述知识,请尝试完成以下实验。
实验:chess.go
- 扩展代码清单16-8,使用字符
kqrbnp
表示上方的黑棋、字符KQRBNP
表示下方的白棋,然后在棋子的起始位置打印出所有棋子。 - 编写一个能够美观地打印出整个棋盘的函数。
- 使用
[8][8]rune
数组而不是字符串来表示棋盘。回忆一下,rune
字面量使用单引号包围,并使用格式化变量%c
进行打印。
速查16-1答案
1.planets[0]
2.未被赋值的数组元素将按照数组的类型被初始化为相应的零值,对整数数组来说,数组元素的零值就是数字0。
速查16-2答案
访问planets[11]
将导致编译器检测到无效的数组索引。
速查16-3答案
planets数组的长度为8。
速查16-4答案
1.使用关键字range
可以让循环变得更简单,并且可以避免诸如i <= len(dwarfs)
这样的索引越界错误。
2.传统的for
循环在你需要定制循环过程的时候更为适用,例如,你可能想要以逆序方式迭代数组,或者每秒访问一个数组元素。
速查16-5答案
1.planetsMarkII
数组是planets
数组的副本,它不会因为planets
的修改而发生变化。
2.terraform
函数可以返回修改后的[8]string
数组,这样main
函数就可以将这个数组重新赋值给planets变量了。除此之外,第17章介绍的切片和第26章介绍的指针同样能够修改数组。
速查16-6答案
使用var grid [9][9]int
即可声明一个9 × 9整数网格。
本文摘自《Go语言趣学指南》
《Go语言趣学指南》是一本面向Go语言初学者的书,循序渐进地介绍了使用Go语言所必需的知识,展示了非常多生动有趣的例子,并通过提供大量练习来加深读者对书中所述内容的理解。本书共分8个单元,分别介绍变量、常量、分支和循环等基础语句,整数、浮点数和字符串等常用类型,类型、函数和方法,数组、切片和映射,结构和接口,指针、nil和错误处理方法,并发和状态保护,并且每个单元都包含相应的章节和单元测试。
《Go语言趣学指南》适合对初学Go语言有不同需求的程序员阅读。无论是刚开始学习Go语言的新手,还是想要回顾Go语言基础知识的Go语言使用者,只要是想用Go做开发,无论是开发小型脚本还是大型程序,《Go语言趣学指南》都会非常有帮助。
关于作者
内森·扬曼(Nathan Youngman) 既是一位自学成才的网络开发者,也是一位终生学习概念的践行者。他是加拿大埃德蒙顿市Go聚会(meetup)的组织者,Canada Learning Code的辅导教师以及狂热的地鼠玩偶摄影爱好者。
罗杰·佩珀(Roger Peppé) 是一位Go贡献者,他维护着一系列开源Go项目,运营着英国纽卡斯尔市的Go聚会,并且当前正在负责Go云端基础设施软件的相关工作。