go 有两种内置的存储列表的方式,本篇介绍其中一种:数组。
数组保存值的集合
数组是所有共享同一类型的值的集合。
数组中包含的值称为它的元素
。
可以有一个字符串数组、一个布尔数组或其他类型的数组(甚至数组的数组)。
可以将整个数组存储在单个变量中,然后访问数组中所需要的任何元素。
数组保存特定数量的元素,不能增长或收缩,要声明保存数组的变量,需要在方括号[]
中指定它所保存的元素数量,后跟数组所保存的元素类型。
要设置数组元素的值或稍后检索值,需要一种方法来指定哪个元素,数组中的元素从0开始编号,一个元素的编号称为其索引
。
例如,创建一个由7个字符串组成的数组:
var notes [7]string
notes[0] = "do" //给第一个元素赋值
notes[1] = "re"
notes[2] = "mi"
fmt.Println(notes[0]) //打印第一个元素
fmt.Println(notes[1]) //打印第二个元素
创建一个整型数组:
var primes [5]int
primes[0] = 2
primes[1] = 3
fmt.Println(primes[0])
time.Time值的数组:
var dates [3]time.Time
dates[0] = time.Unix(1257894000, 0)
dates[1] = time.Unix(1447920000, 0)
dates[2] = time.Unix(1508632200, 0)
fmt.Println(dates[1])
输出: 2015-11-19 08:00:00 +0000 UTC
数组中的零值
与变量一样,当创建一个数组时,其所包含的所有值都被初始化为该数组所保存类型的零值。默认情况下,一个 int 值数组用0填充。
var primes [5]int
primes[0] = 2
fmt.Println(primes[0]) //打印显示赋值的元素
fmt.Println(primes[2]) //打印未显式赋值的元素
fmt.Println(primes[4]) //打印未显式赋值的元素
输出:
2
0
0
字符串的零值是一个空字符串,默认情况下,一个字符串值数组用空字符串填充。
零值可以使操作数组元素变得安全,即使没有显式为其赋值。
例如:一个整数计数器数组,可以在不首先显式赋值的情况下给它们中任何一个赋值,因为其都是从0开始。
var counters [3]int
counters[0]++ //将第一个元素从0增加到1
counters[0]++ //将第一个元素从1增加到2
counters[2]++ //将第三个元素从0增加到1
fmt.Println(counters[0], counters[1], counters[2])
输出:
2
0
1
创建数组时,其所包含的所有值都初始化为数组所保存类型的零值。
数组字面量
若事先知道数组应该保存哪些值,可以使用数组字面量来初始化数组。数组字面量的开头与数组类型类似,其元素的数量将放在方括号中,后跟元素的类型,再后面跟大括号,里面是每个元素应该具有的初始值列表,元素值用逗号分隔。
与之前不同,不是逐个为数组元素赋值,而是使用数组字面量初始化整个数组。
数组字面量
允许使用短变量声明:
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
primes := [5]int{2, 3, 5, 7, 11}
可以将数组字面量分散到多行上,但必须在代码中的每个换行字符前使用逗号,若数组字面量的最后一项后面跟着换行符,需要在其后面跟一个逗号。
text := [3]string{
"this is a series of long strings",
"which would be awkward to place",
"together on a single line", //末尾的逗号是必须的。
}
“fmt"包中的函数如何处理数组
当只想调试代码时,不必逐个将数组元素传递给fmt包中的Println和其他函数,只需传递整个数组。fmt包有做格式化和打印数组的逻辑。(fmt包还可以处理切片、映射和其他数据结构)
Printf和Sprintf函数使用的”%#v"动词,将按照go代码中显示的方式格式化值,当用"%#v"格式化时,数组在结果中显示为go数组字面量。
var notes [3]string = [3]string{"do", "re", "mi"}
var primes [5]int = [5]int{2, 3, 5, 7, 11}
fmt.Printf("%#v\n", notes)
fmt.Printf("%#v\n", primes)
输出:
[3]string{"do", "re", "mi"}
[5]int{2, 3, 5, 7, 11}
在循环里访问数组元素
不必显示地编写代码中要访问的数组元素的整数索引。 可以使用整型变量中的值作为数组索引。
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
index := 1
fmt.Println(index, notes[index]) //打印索引1处的数组元素
index = 3
fmt.Println(index, notes[index]) //打印索引3处的数组元素
这意味着可以使用for循环来处理数组元素之类的操作,循环遍历数组中的索引,并使用循环变量访问当前索引处的元素。
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
for i := 0; i <= 2; i++ {
fmt.Println(i, notes[i])
}
使用"len"函数检查数组长度
写只访问有效数组索引的循环容易出错,有两种方式。
第一种是在访问数组前检查数组中元素的的实际数量,可以使用内置的len
函数来实现,该函数返回数组的长度(其包含的元素个数)。
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
fmt.Println(len(notes)) //打印“notes”数组的长度
primes := [5]int{2, 3, 5, 7, 11}
fmt.Println(len(primes)) //打印“primes”数组的长度
设置循环以处理整个数组时,可以使用len
确定哪些索引可以安全访问。
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
for i := 0; i < len(notes); i++ {
fmt.Println(i, notes[i])
}
若len(notes)返回7,可访问的索引最多是6,因为数组索引从0开始。
使用"for…range"安全遍历数组
在range格式中,提供一个变量,该变量保存每个元素的整数索引,另一个变量保存元素本身的值,以及要循环的数组,循环将为数组中的每个元素运行一次,将元素的索引赋值给第一个变量,将元素的值赋值给第二个变量,然后在循环块中添加代码处理这些值。
这种for循环形式没有混乱的初始化、条件和标志(post)表达式,由于元素值是自动分配给变量的,因此不会有意外访问无效数组索引的风险。
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
for index, note := range notes {
fmt.Println(index, note)
}
对于每个元素,index变量设置为元素的索引,note变量设置为元素的值。
在"for…range"循环中使用空白标识符
go 要求使用声明的每个变量,若停止使用for...range
循环中的变量,将编译错误。
当调用一个具有多个返回值的函数时,忽略其中一个返回值,将该值赋值给空白标识符_
,这会让go丢弃该值,而不会编译错误。
若不需要每个数组元素的索引,可以:
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
for _, note := range notes {
fmt.Println(note)
}
若不需要值变量,也可以使用空白标识符:
notes := [7]string{"do", "re", "mi", "fa", "so", "la", "ti"}
for index, _ := range notes {
fmt.Println(index)
}
获得数组中数字的平均值
package main
import "fmt"
func main() {
numbers := [3]float64{71.8, 56.2, 89.5}
var sum float64 = 0
for _, number := range numbers {
sum += number
}
sampleCount := float64(len(numbers)) //获取类型为int的数组长度,并将其转换为float64
fmt.Printf("Average: %0.2f\n", sum/sampleCount) //将数组值的总和除以数组长度得到平均值
}
读取文本文件
编辑一个data.txt文件,将三个数据写进去:
71.8
56.2
89.5
首先编写一个读取文件的程序:
package main
import (
"bufio"
"fmt"
"log"
"os"
)
func main() {
file, err := os.Open("data.txt") //打开文件进行读取
if err != nil {
log.Fatal(err)
} //若打开文件时出现错误,报告错误并退出
scanner := bufio.NewScanner(file) //为文件创建一个新的扫描器
for scanner.Scan() { //从文件中读取一行
fmt.Println(scanner.Text()) //打印该行
} //循环至文件结尾,scanner.Scan返回false
err = file.Close() //关闭文件以释放资源
if err != nil {
log.Fatal(err)
} //若关闭文件时出现错误,报告错误并退出
if scanner.Err() != nil {
log.Fatal(scanner.Err())
} //若扫描文件时出现错误,报告错误并退出
}
上述程序如何工作:
首先向 os.Open 函数传递一个带有要打开文件的名字的字符串,从 os.Open 会返回两个值:指向代表被打开文件的 os.File 值的指针,以及一个错误值。若错误值为 nil ,则表示文件成功打开,其他任何值都表示存在错误(比如文件丢失或不可读)。若错误,则会记录错误信息并退出程序。
然后将 os.File 值传递给 bufio.NewScanner 函数,将返回一个从文件中读取的 bufio.Scanner 值。
bufio.Scanner 上的 Scan 方法是用来作为 for 循环的一部分。将从文件中读取一行文本,若读取数据成功则返回 true ,否则返回 false 。若将 Scan 用作 for 循环的条件,那么只要有更多的数据需要读取,循环就会继续运行,一旦到达文件的结尾(或出现错误),Scan 将返回 false ,循环退出。
在 bufio.Scanner 上调用 Scan 方法后,调用 Text 方法将返回一个包含已经读取数据的字符串,对这个程序,调用 Println 打印每一行。
一旦循环退出,就完成了对文本的处理,保持文件打开会消耗操作系统的资源,因此当程序完成对文件操作时要对文件进行关闭。对 os.File 调用 Close 方法将完成对文件的关闭。与 Open 函数一样,Close 方法也返回一个 error 值,除非出现错误,否则该值为 nil ,(与 Open 不同,Close 只返回一个值,除了错误外没有其他值可以返回)。
在扫描文件时,bufio.Scanner 也可能遇到错误,调用扫描器上的 Err 方法将返回该错误,在退出之前记录该错误。
将文本数据读入数组
需要将data.txt读取的字符串转换为数字并存储在数组中:
// Package datafile allows reading data sample from files.
package datafile
import (
"bufio"
"os"
"strconv"
)
// GetFloats reads a float64 from each line of a file.
func GetFloats(fileName string) ([3]float64, error) {
var numbers [3]float64
file, err := os.Open(fileName)
if err != nil {
return numbers, err
}
i := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
numbers[i], err = strconv.ParseFloat(scanner.Text(), 64)
if err != nil {
return numbers, err
}
i++
}
err = file.Close()
if err != nil {
return numbers, err
}
if scanner.Err() != nil {
return numbers, scanner.Err()
}
return numbers, nil
}
希望能读取除了data.txt以外的文件,因此接受打开的文件名作为参数,将函数设置为返回两个值,一个float64值数组和一个错误值。只有当错误值为nil时,才应将第一个返回值视为可用。
接下来,声明一个由三个float64值组成的数组,将保存从文件中读取的数字。
与之前代码一样,打开文件进行读取,不同之处在于,打开传递给函数的任何文件名,而不是硬编码的 “data.txt” 字符串,若遇到错误,需要返回一个数组以及错误值,所以只返回numbers数组(尽管还没有为其赋值)。
需要知道将每一行赋值给哪个数组元素,因此创建一个变量来跟踪当前索引。
设置bufio.Scanner和循环遍历文件行的代码与之前的代码相同,但是循环的代码不同,需要对从文件中读取的字符串调用 strconv.ParseFloat 来将其转换为 float64 ,并将结果赋值给数组。若ParseFloat导致了错误,需要返回该错误,若解析成功,需要对 i 增值,以便下一个数被赋值给下一个数组元素。
关闭文件并报告任何错误,若没有错误,将到达 GetFloats 函数末尾,并返回float64值数组以及nil错误。
更新程序读取文本文件
package main
import (
"fmt"
"xxx/xxx/datafile"
"log"
)
func main() {
numbers, err := datafile.GetFloats("data.txt")
if err != nil {
log.Fatal(err)
}
var sum float64 = 0
for _, number := range numbers {
sum += number
}
sampleCount := float64(len(numbers))
fmt.Printf("Average: %0.2f\n", sum/sampleCount)
}
上述程序只有在data.txt中有三行或更少行时才运行,若超过三行则会报错。 因为声明了 numbers 数组来保存3个元素。
Go数组的大小是固定的,不能增长或收缩。下一篇将解决这个问题!