Go语言实践[回顾]教程11--学习成绩统计的示例【下】
需求未变但源数据结构改为 map 类型
在上两节中使用的源数据都是仅有分数没有姓名的一维数组,但实际成绩单中基本都是姓名与分数对照形式出现的,这就与 Go 语言的 map 类型十分相似,再者从数据库取出来的也几乎都是与 map 类型一致,所以,这节我们以 map 类型为源数据,重新完成项目需求。
创建 map 类型的数据源文件
使用前面的方法在本项目 data 目录下创建一个 map_score.go 文件,更新为如下代码:
// 姓名、分数键值对格式的成绩的数据,文件名 map_score.go
package data
var MapScore = map[string]int{
"张三丰": 86,
"郭大壮": 67,
"李晓明": 73,
"王二虎": 98,
"赵慧兰": 90,
"丁菲菲": 56,
"孙艳玲": 49,
"周喜庆": 77,
"高建国": 89,
"吴亚芬": 96,
}
这个格式其实与其他很多语言类似,在 Go 语言中叫 map(映射),其实与 PHP、JavaScript 中的键值对数组、Python 中的字典都很相似。核心特点就是键值对,元素是一组有键(Key)有值(Value)的结构。
第4行,var MapScore = map[string]int 表示声明定义一个键是字符串类型,值是整数类型的映射类型变量 MapScore。
注意第4行的变量名 MapScore 首字母一定要大写,否则其他包无法使用。
创建操作 map 类型数据进行统计排序的源文件
使用前面的方法在本项目 count 目录下创建一个 map_data_count.go 文件,更新为如下代码:
// 统计来自 map 类型的数据,文件名 map_data_count.go
package count
import (
"fmt"
"score_count/data"
"sort"
)
// MapDataCount 对一维数组进行排序、统计60分及以上人数,前三名分数
func MapDataCount() {
// 获取数组长度赋值给 len 以免后面多处使用重复计算
len := len(data.OnlyScore)
// 声明一个新的结构体数据结构类型 student
type student struct {
Name string
Score int
}
// 声明一个空切片,用于保存正序结果
var positiveArr []student
// 将源 map 内的数据全部以 student 结构复制到切片 positiveArr 中
for k, v := range data.MapScore {
positiveArr = append(positiveArr, student{k, v})
}
// 使用标准库提供的正序排序函数给切片 positiveArr 排序
sort.Slice(positiveArr, func(i, j int) bool {
return positiveArr[i].Score < positiveArr[j].Score // 通过判断决定升序
})
// 创建一个与源数据数组同样长同样类型的切片,用于存放倒序结果
reverseArr := make([]student, len)
passNum := 0 // 用于保存及格人数
levelA, levelB, levelC, levelD := 0, 0, 0, 0
// 将排序好的数组颠倒顺序,使用for循环综合性能更好
// 统计及格数量刚好也要用for循环,干脆放在一个排序后的循环里
for i := 0; i < len; i++ {
v := positiveArr[i].Score
reverseArr[len-i-1] = positiveArr[i]
// // 利用正序排列特点,综合判断统计各分数段
if levelD == 0 && v >= 60 {
passNum = len - i
levelD = i
} else if levelC == 0 && v >= 75 {
levelC = i - levelD
} else if levelB == 0 && v >= 90 {
levelB = i - levelC - levelD
levelA = len - i
}
}
// 对倒序排序后的成绩数组直接切片前三个元素就刚好是前三名的成绩
topThree := reverseArr[:3]
fmt.Println("及格人数:", passNum)
fmt.Printf("及格率为: %d%%\n", passNum*100/len)
fmt.Println("前三分数:", topThree)
fmt.Println("成绩正序:", positiveArr)
fmt.Println("成绩倒序:", reverseArr)
fmt.Println("A 级人数:", levelA)
fmt.Println("B 级人数:", levelB)
fmt.Println("C 级人数:", levelC)
fmt.Println("D 级人数:", levelD)
}
以上代码运行结果正确。
由于整体代码逻辑与上一节的相同,只是更换的了数据的格式,所以这里仅针对处理与 map 相关的变动做说明。
第16~19行:通过结构体声明一个自定义的数据结构类型,一个结构体内可以有不同类型的成员。
第16行:type 是声明类型的关键字;student 自定义的类型名;struct 声明结构体的关键字,表示是多个数据的集合。
第17行:Name 表示结构体中的一个成员名称叫 Name;string 表示成员 Name 的数据类型为字符串。
第18行:Score 表示结构体中的一个成员名称叫 Score;int 表示成员 Score 的数据类型为整数。
注意,成员名的首字母大写,结构体外部才可以使用这个成员。
因为 map 类型不支持直接排序操作,所以要先把 map 类型的源数据转成切片才可以使用 sort 包排序。要转成切片(本质是数组),需要先把 map 的键值对封装成一个新的类型,才可以作为一个元素追加到切片中。这就是为什么要先声明一个结构体的原因。
第22行:声明了一个 student 类型的切片 positiveArr,用于保存从源数据 map 中复制过来的数据(以新建的数据类型 student 添加的),然后会给这个切片排序(正序)。
第25~27行:将 map 中的源数据循环添加到 positiveArr 切片中。
第25行:在 for 循环中使用 range 关键字遍历 data.MapScore 源数据 map,每次都返回 map 的键和值分别赋值给 k 和 v。
第26行:在循环体中,使用 append() 方法,依次向 positiveArr 切片尾部追加 student 结构类型的数据,数据内容是 student 的 Name 成员的值是循环得到 map 元素的键名 k,student 的 Score 成员的值是循环得到 map 元素的值 v。然后返回一个新的切片给 positiveArr。
循环结束后,positiveArr 切片的内部结构其实就是可以看做一个 student 类型的一维数组了。
第30~32行:使用标准库中的 sort.Slice() 函数给 positiveArr 切片排序。
第30行:sort.Slice() 函数的功能是自定义排序。第一个参数是待排序的切片;第二个参数是个排序判断方法的匿名函数,返回真或假,表示两个元素是否交换位置。其中匿名函数中的 i 表示后一个元素(当前)索引,j 表示前一个元素索引。
第31行:表示返回 当前元素(结构体)的 Score 成员值是否大于上一元素的 Score 成员值 的关系判断结果(真或假),决定是否交换元素内容,就等于是否更改排列顺序。
第35行:创建一个 student 类型的切片,长度与源数据 map 相同。
第44行:切片 positiveArr 当前元素的 Score 成员才是分数,因为当前元素是个结构体了。
本节小结
以下是对本节涉及的 Go 语言编程内容的归纳总结,方便记忆:
● map,是引用的集合类型,其元素是成对出现的,也就是键值对,一个键名对应一个值。其实也可以理解为是一个较特殊的数组,只不过它的索引是键名,不是顺序号。map 是无序排列的,所以不能用顺序号取值。
var 变量名 map[键类型]值类型 是 map 的典型声明格式。声明时不需要知道 map 的长度,因为 map 是可以动态增长的,未初始化的 map 的值是 nil,len() 函数可以获取 map 中 键值对 的数量。
var m map[string]int 表示声明一个键是字符串值是整数的 map 变量 m。
var m = map[string]int{“A”:25, “B”:30} 表示声明一个键是字符串值是整数的 map 变量 m,并初始化赋值两个键值对元素,A的值是25,B的值是30。这是 len(m) 的结果是 2。
m.[“A”] 表示获取 map 变量 m 中 A 键的值,也就是 25。
m.[“A”] = 66 表示将 66 赋值给 map 变量 m 中 A 键的值。
● 结构体,是带有成员的复合类型,每个成员都有自己的名字,各成员的值类型可以不同。主要用于创建一个新的类型(数据结构)。
type tm struct {
X string
Y int
}
这是结构体的典型声明格式,tm 是结构体类型名,注意 tm 在这里代表一个数据类型了。type 关键字表示要声明一个类型,struct 关键字表示定义一个结构体(数据结构)。X、Y 是自定义的成员名(不需要引号),string 是成员 X 的数据类型,int 是成员 Y 的数据类型。
var m tm,tm不能直接当变量使用,因为它是一个数据类型,需要用它声明一个变量才可以发挥作用。所以这表示声明一个 tm 类型的变量 m。
m.X,表示获取 m 下的成员 X 的值。
m.X = “one”,表示将 m 下的成员 X 的值修改为 “one”。
● 匿名函数,就是没有函数名的函数,申明格式如下:
func(参数1, 参数n) 返回值类型 {
…函数体…
}
由于匿名函数没有函数名,无法通过名称来调用,所以通常都是直接写在需要调用的地方,如需要回调函数的地方等。
● sort.Slice(),是标准库 sort 包中的自定义排序函数,如何排序是通过回调函数实现的。
func Slice(s interface{}, rule func(i, j int) bool),s 是待排序的切片数据;rule 是回调函数,通过返回布尔值决定是否调换元素位置;i 和 j 都是这个函数的参数,i 表示内部循环的当前索引,j 表示内部循环的当前索引的前一个索引。这是使用了命名函数,也就是函数声明定义在其他地方单独写。
func Slice(s interface{}, func(i, j int) bool {}),这种格式与上面差别就是使用了匿名函数,去掉了函数名,将函数体直接写在了后面的花括号里。
● append(),用于为切片追加数据,每次追加的数据都是在最后一个元素的后面,将返回一个新的切片。
append(s, “new”),表示将字符串“new”追加到切片 s 尾部。
append(s, 1, 5, 22),表示将整数 1、5、22 追加到切片 s 尾部。
append(s, []int{1, 5, 22}),表示将整数类型包含 1、5、22 元素的切片追加到切片 s 尾部,会自动解开按元素追加。
要注意追加的数据类型与原切片的数据类型一致。
.
.
上一节:Go/Golang语言学习实践[回顾]教程10–学习成绩统计的示例【中】