5.数组

Saturday, July 15, 2023

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数组的大小是固定的,不能增长或收缩。下一篇将解决这个问题!

Golang打怪升级

6.切片

4.代码集:包

comments powered by Disqus