当需要使用一个特定的值,为了找到它,需要从数组或切片中查看每一个元素,有一种集合,其中的每个值都有一个标签,那么就可以快速找到需要的值,
映射
就是做这个工作的。
从文件中读取名字
有一个文件 votes.txt :
Amber Graham
Brian Martin
Amber Graham
Brian Martin
Amber Graham
每一行代表一次投票,需要处理文件的每一行并统计每个名称出现的总次数,获得多票的胜出。
首先需要读取votes.txt文件的内容,之前有GetFloats函数来读取文件中的每一行,并转换为一个切片,但只能读取float64值,所以需要增加一个GetStrings函数,把每行作为string值添加到返回的切片中。
package datafile
import (
"bufio"
"os"
)
func GetStrings(fileName string) ([]string, error) {
var lines []string
file, err := os.Open(fileName)
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
lines = append(lines, line)
}
err = file.Close()
if err != nil {
return nil, err
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
return lines, nil
}
package main
import (
"fmt"
"xxx/xxx/datafile"
"log"
)
func main() {
lines, err: = datafile.GetStrings("votes.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(lines)
}
使用困难的切片方法对名字计数
上述代码从文件中读取名字,如何计算名字出现了多少次?有两种方式,一种是切片,另一种是数据结构映射。
关于使用切片的方式:创建两个切片,每个的长度都是元素的总个数,并是指定的顺序,第一个切片用来保存在文件中找到的名字,每个名字只出现一次,命名为 names;第二个切片命名为counts,保存文件中的名字出现的次数,元素 counts[0] 保存 names[0] 出现的次数,以次类推。
func main() {
lines, err := datafile.GetStrings("votes.txt")
if err != nil {
log.Fatal(err)
}
var names []string //这个变量保存人名
var counts []int //这个变量保存每个人名出现的次数
for _, line := range lines {
matched := false
for i, name := range names { //循环处理names切片中的每一个值
if name == line { //若line与当前名称匹配
counts[i]++ //增加对应的count值
matched = true //标识找到了匹配的名字
}
}
if matched == false { //若没有找到匹配名字
names = append(names, line) //添加它作为一个新名字
counts = append(counts, 1) //并增加一个新的计数(这行是第一次出现)
}
}
for i, name := range names {
fmt.Printf("%s: %d\n", name, counts[i]) //输出names切片中的每一个元素,和对应的counts切片中的元素
}
}
程序使用一个循环嵌套在另一个循环中的方式来统计名字的次数,外面的循环把文件中的每行以每次一行的方式赋值给line变量。内部循环通过遍历names切片中的每个元素来查找与文件中的当前行匹配的名称。
若某人在选票上写一个名字,会导致文件中加载一个字符串,程序会一个一个确认names的元素中是否有等于这个名字。若没有找到匹配项,程序会在names切片末尾追加这个名字,并在counts切片中相应的位置增加1。假如下一行的这个名字已经存在于names切片中,程序会找到位置,在对应位置增加1。
映射
保存名字使用的是切片,对于文件中的每一行,必须在许多names切片的值中寻找来进行比较,这样会导致性能差。
go有一种方法保存数据集合: 映射
。 一个映射是通过键
来访问每一个值的集合,键是一个简单的方式从映射中找出数据。
相对于数组和切片只能使用整型数字作为索引,映射可以使用任意类型的键(只要这个类型可以使用 == 来比较),包括数字、字符串和其他。所有的值只能是相同的类型,所有的健也需要是相同的类型,但是键和值的类型不必相同。
声明一个保存映射的变量,需要 map
关键字,后跟一对包含键类型的方括号[]
,然后在方括号后跟提供值的类型。
var myMap map[string]float64
与切片一样,声明一个映射变量并不会自动创建一个映射,需要调用make函数(与创建切片相同的函数),可以传递给make想要创建的映射类型(与需要赋值到的类型相同)。
var ranks map[string]int //声明一个映射变量
ranks = make(map[string]int) //真正创建一个映射
短变量声明方式:
ranks := make(map[string]int) //创建一个映射并声明一个用于保存它的变量
映射的赋值与取值语法跟数组和切片类似,但数组和切片仅允许使用整型作为元素索引,映射可以选择几乎所有的类型来作为键。
这是一个名为ranks的映射:
ranks["gold"] = 1
ranks["silver"] = 2
ranks["bronze"] = 3
fmt.Println(ranks["bronze"])
fmt.Println(ranks["gole"])
一个键和值都使用string的映射:
elements := make(map[string]string)
elements["H"] = "Hydrogen"
elements["Li"] = "Lithium"
fmt.Println(elements["Li"])
fmt.Println(elements["H"])
使用整型作为键,bool类型作为值:
isPrime := make(map[int]bool)
isPrime[4] = false
isPrime[7] = true
fmt.Println(isPrime[4])
fmt.Println(isPrime[7])
映射字面量
若预先知道映射的键和值,可以使用字面量来创建映射,映射字面量是以映射类型(以“映射[键类型]值类型”的形式)开始,后面跟花括号,内含想要映射初始就包含的 键/值 对。
myMap := map[string]float64{"a": 1.2, "b": 5.6}
例:
ranks := map[string]int{"bronze": 3, "silver": 2, "gold": 1}
fmt.Println(ranks["gold"])
fmt.Println(ranks["bronze"])
elements := map[string]string{
"H": "Hydrogen",
"Li": "Lithium",
} //多行映射字面量
fmt.Println(elements["H"])
fmt.Println(elements["Li"])
与切片字面量一样,花括号为空创建一个空的映射:
emptyMap := map[string]float64{}
映射中的零值
对于数组和切片,若访问一个没有赋值过的键,会得到一个零值,根据值类型不同,零值不一定是0。比如映射的值类型是string,零值就是空字符串。
与数组和切片一样,零值可以更安全地修改映射的值,即使在没有给它赋值的情况下。
counters := make(map[string]int)
counters["a"]++
counters["a"]++
counters["c"]++
fmt.Println(counters["a"], counters["b"], counters["c"])
映射变量的零值是nil
与切片一样,映射变量的零值是nil,若声明了一个映射变量但未赋值,是nil,意味着没有映射存在来增加键或值。
var nilMap map[int]string
fmt.Printf("%#v\n", nilMap)
nilMap[3] = "three" //映射是nil,无法添加新值,会导致panic
在增加一个新的键值对前,需要使用make或映射字面量来创建一个映射,并赋值给映射变量。
var myMap map[int]string = make(map[int]string)
myMap[3] = "three"
fmt.Printf("%#v\n", myMap)
如何区分已经赋值的值和零值
虽然零值很有用,但无法判断一个键是被赋值成了零值还是未赋值。
有一个例子:错误输出了名为 Carl 的人没有通过,实际上并没有记录他的成绩。
func status(name string) {
grades := map[string]float64{"Alma": 0, "Rohit": 86.5}
grade := grades[name]
if grade < 60 {
fmt.Printf("%s is failing!\n", name)
}
}
func main() {
status("Alma") //一个映射中已经被赋值为0的键
status("Carl") //一个映射中未被赋值的键
}
输出:
Alma is failing!
Carl is failing!
为解决此问题,访问映射键的时候可选地获取第2个布尔类型的值,若这个键已经被赋过值,返回true,否则返回false。通常情况下,开发者会将这个布尔值赋给一个名为ok
的变量。
counters := map[string]int{"a": 3, "b": 0}
var value int
var ok bool
value, ok = counters["a"] //访问一个已经被赋值过的值
fmt.Println(value, ok) //ok会返回true
value, ok = counters["b"] //访问一个已经被赋值过的值
fmt.Println(value, ok) //ok会返回true
value, ok = counters["c"] //访问一个未赋值过的值
fmt.Println(value, ok) //ok会返回false
若仅仅要测试值是否存在,可以通过将其赋值给 _
空白标识符来忽略值。
counters := map[string]int{"a": 3, "b": 0}
var ok bool
_, ok = counters["b"] //测试值是否存在,但忽略值
fmt.Println(ok)
_, ok = counters["c"] //测试值是否存在,但忽略值
fmt.Println(ok)
第二个返回值可以用来判断如何处理这个值,是已经赋值了但是正好等于零值,还是从未被赋值过。
修改后:在输出不及格前测试请求的名字是否已经被赋值过。
func status(name string) {
grades := map[string]float64{"Alma": 0, "Rohit": 86.4}
grade, ok := grades[name]
if !ok {
fmt.Printf("No grade recorded for %s.\n", name)
} else if grade < 60 {
fmt.Printf("%s is failing!\n", name)
}
}
func main() {
status("Alma")
status("Carl")
}
练习:
data := []string{"a", "c", "e", "a", "e"} //需计算在切片中的每个字母的次数
counts := make(map[string]int) //保存计数的映射
for _, item := range data {
counts[item]++ //增加当前字母的次数
} //处理每个字符
letters := []string{"a", "b", "c", "d", "e"} //是否这些字符都在映射中存在
for _, letter := range letters {
count, ok := counts[letter] //获取当前字符的计数以及是否出现的指示
if !ok { //如果字母未找到
fmt.Printf("%s: not found\n", letter)
} else { //如果字母找到
fmt.Printf("%s: %d\n", letter, count)
}
}
输出:
a:2
b:not found
c:1
d:not found
e:2
使用"delete"函数删除键/值对
在分配了值之后,某个时候希望将其从映射中移除,go提供了内建的delete
函数,只需传递给delete两个参数:希望删除数据的映射和希望删除的键,然后键和其关联的值都会被删除。
如下代码,给两个映射的键分配了值,然后将其删除,之后,尝试访问,获取到零值。(对于ranks映射是0,对于isPrime映射是false)。第二个布尔返回值也是false,说明键已经被删除。
var ok bool
ranks := make(map[string]int)
var rank int
ranks["bronze"] = 3 //给“bronze”键分配值
rank, ok = ranks["bronze"] //由于值存在,ok会返回true
fmt.Printf("rank: %d, ok: %v\n", rank, ok)
delete(ranks, "bronze") //删除键“bronze”和相关的值
rank, ok = ranks["bronze"] //由于值已经被删除了ok返回false
fmt.Printf("rank: %d, ok: %v\n", rank, ok)
isPrime := make(map[int]bool)
var prime bool
isPrime[5] = true //给键5分配值
prime, ok = isPrime[5] //由于值存在,ok会返回true
fmt.Printf("prime: %v, ok: %v\n", prime, ok)
delete(isPrime, 5) //删除键5和相关的值
prime, ok = isPrime[5] //由于值被删除,ok会返回false
fmt.Printf("prime: %v, ok: %v\n", prime, ok)
输出:
rank: 3, ok: true
rank: 0, ok: false
prime: true, ok: true
prime: false, ok: false
使用映射来更新程序
package main
import (
"fmt"
"xxx/xxx/datafile"
"log"
)
func main() {
lines, err := datafile.GetStrings("votes.txt")
if err != nil {
log.Fatal(err)
}
counts := make(map[string]int) //声明一个以人名为键,次数为值的映射
for _, line := range lines {
counts[lines]++ //为人名增加计数
}
fmt.Println(counts) //输出填充的映射
}
对映射进行"for…range"循环
为了从映射中格式化每个键和值作为一行,需要使用循环遍历映射中的每一条。
与数组和切片的 for…range 循环一样,与将一个整数索引赋值给第一个变量不同,映射将键赋给了第一个变量。
for…range 循环让遍历映射中的键和值更方便,仅用一个变量保存键,再用一个变量保存值,并会自动遍历映射中的每一个条目。
package main
import "fmt"
func main() {
grades := map[string]float64{"Alma": 74.2, "Rohit": 86.5, "Carl": 59.7}
for name, grade := range grades {
fmt.Printf("%s has a grade of %0.1f%%\n", name, grade) //打印每一个键和它对应的值
} //循环遍历每一个键/值对
}
输出:
Carl has a grade of 59.7%
Alma has a grade of 74.2%
Rohit has a grade of 86.5%
若只需要循环所有的键,可以忽略对应的值变量:
fmt.Println("Class roster:")
for name := range grades {
fmt.Println(name)
}
输出:
Class roster:
Alma
Rohit
Carl
若只需要值,可以将键赋 _ 空白标识符:
fmt.Println("Grades:")
for _, grade := range grades {
fmt.Println(grade)
}
输出:
Grades:
59.7
74.2
86.5
这里有个问题,如果将之前的结果存入一个文件,并执行,会发现映射的键和值是按照随机顺序打印,若多次执行,每次的结果顺序是不一样的。
“for…range"循环以随机顺序处理映射
for…range 以随机的顺序遍历映射的键和值,因为映射是一个非有序的键/值对集合。但有时需要按照特定的顺序遍历时,要自己写一些代码了。
例子:使名字按照字母表的顺序输出,使用了两个for循环,第一个循环遍历映射里边所有的键,忽略值,并把其增加到一个字符串的切片上,然后把切片传递给sort包中的Strings函数来以字母表顺序排序。第二个for循环并不遍历映射,而是遍历变量排序后的名字的切片,输出名字,并从映射中获取与名字对应的值,仍然处理映射中的每一个键和值,但是从已排序好的切片中获取而不是从映射中获取的键。
package main
import (
"fmt"
"sort"
)
func main() {
grades := map[string]float64{"Alma": 74.2, "Rohit": 86.5, "Carl": 59.7}
var names []string
for name := range grades {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s has a grade of %0.1f%%\n", name, grades[name])
}
}
按照以上代码执行,名字是按照字母表顺序排列,不管执行多少次都是如此。
若不在乎映射中的数据如何处理,使用for…range循环就可以,但若是需要顺序,就需要自己写代码处理排序问题。
使用for…range更新程序
将打印整个映射的行用for…range替换,把键赋给name变量,把映射的值赋值给count变量,然后用printf输出人名和得票数。
package main
import (
"fmt"
"xxx/xxx/datafile"
"log"
)
func main() {
lines, err := datafile.GetStrings("votes.txt")
if err != nil {
log.Fatal(err)
}
counts := make(map[string]int)
for _, line := range lines {
counts[line]++
}
for name, count := range counts {
fmt.Printf("Votes for %s: %d\n", name, count)
}
}
当可用的数据集合是数组和切片时,需要很多额外的代码和处理时间来查找,但使用映射处理起来更简便。
当需要查找集合中的值的时候,可以考虑映射!