文章目录
- 一、引言
- 二、题目
- 三、实现
- 1.版本一
- 2.版本二
- 3.版本三
- 4.版本四
- 5.版本五
- 6.版本六
- 四、总结与思考
一、引言
如何检验自己对一门语言是否入门?基于这门语言设计一款学生成绩管理系统就是一个很好的评判标准。今天就一起考察下咱们对go的掌握程度吧
二、题目
背景:学生成绩管理系统是用于录入学生成绩信息并支持查询的系统,大体流程如下
可能有些人会觉得,咦,怎么这么简单?简单就对了,因为只有这样咱们才能将注意力集中在go的基本知识上面。接下来就开始我们的实现吧
三、实现
在开始看之前,请尝试自己实现一个版本,这样再来品尝下面的文章效果更佳~
1.版本一
第一个版本咱们仅实现最基本也是最核心的成绩录入功能,其他的先暂时忽略。
这个版本的实现需要考虑以下几点
- 如何将学生的属性封装到一块
- 如何获取控制台输入数据并赋值给学生
接下来看看下面的代码实现
go">package mainimport "fmt"//支持用户录入成绩 1. 通过struct记录每个学生的各项数据
type Student struct {id int64name stringage intchinese, math, english float32 //类型想同的话可以这样简写
}func main() {var id int64fmt.Println("请录入学生学号:")fmt.Scanln(&id)var name stringfmt.Println("请录入学生名称:")fmt.Scanln(&name)var age intfmt.Println("请录入学生年龄:")fmt.Scanln(&age)var chinese float32fmt.Println("请录入学生语文成绩:")fmt.Scanln(&chinese)var math float32fmt.Println("请录入学生数学成绩:")fmt.Scanln(&math)var english float32fmt.Println("请录入学生英语成绩:")fmt.Scanln(&english)student1 := Student{id, name, age, chinese, math, chinese}fmt.Println("学生成绩录入完毕,该学生信息为:", student1)
}
可以看到代码实现比较简陋,这里我们采用struct封装学生的各个属性,然后通过fmt的提供的函数读取用户的输入信息,在输入完毕后就进行打印出来,绘成图如下
接下来让咱们看看执行的效果
通过上面可以看到此时已经完成了最基本了信息录入功能。简单小结如下
- 完成最基本的成绩录入功能,涉及知识点:变量、struct、指针
- 缺点:仅支持单个学生的录入
2.版本二
上个版本咱们的录入功能仅支持单个学生的录入,那么这个版本咱们要进阶一下,支持录入多个学生的信息。因此这个版本咱们需要考虑以下几个问题
- 代码如何支持录入多个学生的信息
- 如何存多个学生的信息
- 如何简化录入信息的消息
接下来看看下面的代码实现
go">package mainimport "fmt"type Student struct {id int64name stringage intchinese, math, english float32 //类型想同的话可以这样简写
}func main() {var studentarray [10]Studentvar num intfmt.Println("请录入要录入的学生数量:")fmt.Scanln(&num)for i := 0; i < num; i++ {var id int64var name stringvar age intvar chinese, math, english float32fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &chinese, &math, &english)student := Student{id, name, age, chinese, math, english}fmt.Println("当前学生成绩录入完毕,该学生信息为:", student)studentarray[i] = student}result := studentarray[0:num]fmt.Println("所有学生成绩录入完毕,信息为:", result)
}
在这里咱们用到了for这个流程控制的关键字,通过它可以实现多次录入的效果,同时这里采用了go自带的数组容器来进行存放多个学生成绩信息,最后是通过Scanf的方式来简化了录入成绩的工作,绘制成图如下
让我们继续来看看最终的执行效果
通过上面可以看到此时已经完成了多个学生录入功能。简单小结如下
- 完成支持多个学生成绩录入功能,拓展知识点:array、for
- 缺点:仅支持成绩录入,不支持查询
3.版本三
上个版本咱们的录入功能支持了多个学生的录入,那么这个版本咱们继续进阶一下,支持查询的功能。这个版本咱们要考虑以下几个问题
- 如何存学生信息用于索引查询
- 如何更好的对代码进行封装
- 如何根据不同的操作(增加、查询)进行对应的处理
接下来看看下面的代码实现
go">package mainimport "fmt"type Student struct {id int64name stringage intchinese, math, english float32 //类型想同的话可以这样简写
}var studentinfo map[int64]Studentfunc search() {var id int64fmt.Println("请录入要查询的学生学号:")fmt.Scanln(&id)student, ok := studentinfo[id]if ok == true {fmt.Println("查到该学生,此学生的详细信息为:", student)return}fmt.Println("未找到该学生信息")
}func add() {var studentarray [10]Studentvar num intfmt.Println("请录入要录入的学生数量:")fmt.Scanln(&num)for i := 0; i < num; i++ {var id int64var name stringvar age intvar chinese, math, english float32fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &chinese, &math, &english)student := Student{id, name, age, chinese, math, english}fmt.Println("当前学生成绩录入完毕,该学生信息为:", student)studentarray[i] = studentstudentinfo[id] = student}result := studentarray[0:num]fmt.Println("所有学生成绩录入完毕,信息为:", result)
}func list() {fmt.Println("所有学生信息如下\n", studentinfo)
}func main() {studentinfo = make(map[int64]Student)for {var operator intfmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")fmt.Scanln(&operator)switch operator {case 1:add()case 2:search()case 3:list()default:fmt.Println("输入不符合预期,不进行任何操作")}}
}
在这里我们可以看到可以通过全局map的方式来存储学生的成绩信息,key为学生学号,value是存储学生信息的struct。同时针对不同的操作抽成对应的func,这样大幅提升了代码的可读性,最后咱们通过switch关键字对不同的操作选择了对应的处理func。那么到这里咱们的第三个版本就算是开发好了,接下来让咱们来演示下看看吧
通过上面的流程,可以看到已经完成了用户信息录入、根据学号查询成绩、列出所有学生信息的功能了。此时虽然好像也能用,但是是不是觉得有什么问题?发现没有,咱们的数据现在是存在内存中的,当服务出现故障或者机器重启就会丢失,那么咱们的用户又要辛苦的重新录入数据。这里简单小结下
- 完成成绩录入查询功能,拓展知识点:map、switch、func函数
- 缺点:服务器重启数据会丢失
4.版本四
上个版本咱们的录入功能支持了录入和查询,这个版本需要考虑以下几个问题
- 如何实现数据持久化
- 如何避免用户使用系统时阻塞数据持久化,或者持久化阻塞用户使用
接下来看看下面的代码实现
go">package mainimport ("bufio""encoding/json""flag""fmt""io""log""os""time"
)type Student struct {Id int64Name stringAge intChinese, Math, English float32 //类型想同的话可以这样简写
}var studentinfo map[int64]Studentfunc search() {var id int64fmt.Println("请录入要查询的学生学号:")fmt.Scanln(&id)student, ok := studentinfo[id]if ok == true {fmt.Println("查到该学生,此学生的详细信息为:", student)return}fmt.Println("未找到该学生信息")
}func add() {var studentarray [10]Studentvar num intfmt.Println("请录入要录入的学生数量:")fmt.Scanln(&num)fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")for i := 0; i < num; i++ {var id int64var name stringvar age intvar Chinese, Math, English float32fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &Chinese, &Math, &English)student := Student{id, name, age, Chinese, Math, English}studentarray[i] = studentstudentinfo[id] = student}result := studentarray[0:num]fmt.Println("所有学生成绩录入完毕,信息为:", result)
}func list() {fmt.Println("所有学生信息如下\n", studentinfo)
}func persistent() {for {//定期将map的数据持久化到本地文件 student_info.txt上time.Sleep(10 * time.Second)fmt.Println("========当前是持久化协程,准备进行数据持久化=========")f, err := os.Create("student_info.txt")if err != nil {fmt.Println(err)return}fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)var count intfor key, student := range studentinfo {count++fmt.Println("当前key是:", key)fmt.Println("当前student是:", student)res, err := json.Marshal(student)fmt.Println("序列化后的结果是:", string(res))if err != nil {fmt.Println(err)}fmt.Println(string(res))_, writeErr := f.WriteString(string(res) + "\n")if writeErr != nil {fmt.Println(err)f.Close()return}}fmt.Println("本次共持久化学生数量为:", count)fmt.Println(" written successfully")err = f.Close()if err != nil {fmt.Println(err)return}fmt.Println("========当前是持久化协程,成功完成数据持久化=========")}
}func recoverInfo() {//从持久化中进行数据恢复fptr := flag.String("fpath", "student_info.txt", "file path to read from")flag.Parse()f, err := os.Open(*fptr)if err != nil {log.Fatal(err)}defer func() {if err = f.Close(); err != nil {log.Fatal(err)}}()r := bufio.NewReader(f)for {n, _, err := r.ReadLine()if err == io.EOF {fmt.Println("finished reading file")break}if err != nil {fmt.Printf("Error %s reading file", err)}fmt.Println(string(n))var student StudentunmarshalErr := json.Unmarshal(n, &student)if unmarshalErr != nil {fmt.Println("反序列化失败:", unmarshalErr)}fmt.Println("当前的信息为:", studentinfo)studentinfo[student.Id] = student}
}func main() {studentinfo = make(map[int64]Student)recoverInfo()go persistent()for {var operator intfmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")fmt.Scanln(&operator)switch operator {case 1:add()case 2:search()case 3:list()default:fmt.Println("输入不符合预期,不进行任何操作")}}
}
这里采用了go的io周期性的将内存中的学生数据持久化到文件中,并在服务启动时再去文件中读取数据到内存,从而避免了重新录入数据。同时在这里我们通过go原生并发的语法 go func()的方式新开一个协程去处理出久化的事情,这样相当于用户使用系统跟数据持久化是两个人在处理,彼此之间互相不影响。接下来一起演示一遍看看
从上面可以看到咱们录入的数据已经被写到磁盘文件了,那么再来看看启动的时候是否有正确加载到内存中呢
可以看到,当咱们重新启动程序时,之前录入的数据也是存在的并支持正常的搜索。这基本上满足咱们对学生成绩管理系统的设想了对吧?那从技术角度想想是否还存在什么问题或者待优化的空间呢,大家发现没有,现在的数据会周期的写到磁盘文件进行持久化,那如果刚录入完数据到文件持久化这个阶段服务出故障了呢?此时数据就会丢失;另一方面当录入的学生数量过大时例如上万个学生,此时每次全量写磁盘性能相对来说就会比较差并且也是没有必要的。小结如下
- 完成数据的持久化功能,拓展知识点:文件读写、goroutinue、defer、序列化/反序列化、异常处理
- 缺点:每次都要全量将数据进行持久化,性能不太好并且有丢数据的可能,同时代码有待优化空间
5.版本五
上个版本咱们已经基本完成了开发,这个版本需要考虑以下几个问题
- 如何避免每次全量写以及可能会导致丢数据的问题
接下来看看下面的代码实现
go">package mainimport ("bufio""encoding/json""flag""fmt""io""log""os""time"
)type Student struct {Id int64Name stringAge intChinese, Math, English float32 //类型想同的话可以这样简写
}var studentinfo map[int64]Studentfunc search() {var id int64fmt.Println("请录入要查询的学生学号:")fmt.Scanln(&id)student, ok := studentinfo[id]if ok == true {fmt.Println("查到该学生,此学生的详细信息为:", student)return}fmt.Println("未找到该学生信息")
}func add(studentChan chan Student) {var studentarray [10]Studentvar num intfmt.Println("请录入要录入的学生数量:")fmt.Scanln(&num)fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")for i := 0; i < num; i++ {var id int64var name stringvar age intvar Chinese, Math, English float32fmt.Scanf("%d %s %d %f %f %f", &id, &name, &age, &Chinese, &Math, &English)student := Student{id, name, age, Chinese, Math, English}studentarray[i] = studentstudentinfo[id] = studentstudentChan <- student}result := studentarray[0:num]fmt.Println("所有学生成绩录入完毕,信息为:", result)
}func list() {fmt.Println("所有学生信息如下\n", studentinfo)
}func persistent() {for {//定期将map的数据持久化到本地文件 student_info.txt上time.Sleep(1 * time.Minute)fmt.Println("========当前是持久化协程,准备进行数据持久化=========")f, err := os.Create("student_info.txt")if err != nil {fmt.Println(err)return}fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)var count intfor key, student := range studentinfo {count++fmt.Println("当前key是:", key)fmt.Println("当前student是:", student)res, err := json.Marshal(student)fmt.Println("序列化后的结果是:", string(res))if err != nil {fmt.Println(err)}fmt.Println(string(res))_, writeErr := f.WriteString(string(res) + "\n")if writeErr != nil {fmt.Println(err)f.Close()return}}fmt.Println("本次共持久化学生数量为:", count)fmt.Println(" written successfully")err = f.Close()if err != nil {fmt.Println(err)return}fmt.Println("========当前是持久化协程,成功完成数据持久化=========")}
}func innerAppendPersistent(student Student) {file, err := os.OpenFile("student_info.txt", os.O_WRONLY|os.O_APPEND, 0666)if err != nil {fmt.Println("文件打开失败", err)return}defer file.Close()res, err := json.Marshal(student)fmt.Println("序列化后的结果是:", string(res))if err != nil {fmt.Println(err)}//写入数据时使用带有缓冲的方式write := bufio.NewWriter(file)write.WriteString(string(res) + "\n")write.Flush()fmt.Println("当前学生信息追加成功", student)
}func appendPersistent(studentChan chan Student) {//增量持久化for {time.Sleep(250 * time.Millisecond)select {case student := <-studentChan:fmt.Println("接收到信息:", student)innerAppendPersistent(student)breakdefault://fmt.Println("没有接收到任何值")}}
}func recoverInfo() {//从持久化中进行数据恢复fptr := flag.String("fpath", "student_info.txt", "file path to read from")flag.Parse()f, err := os.Open(*fptr)if err != nil {log.Fatal(err)}//defer的作用是?会在函数返回前进行调用,类似钩子函数defer func() {if err = f.Close(); err != nil {log.Fatal(err)}}()r := bufio.NewReader(f)for {n, _, err := r.ReadLine()if err == io.EOF {fmt.Println("finished reading file")break}if err != nil {fmt.Printf("Error %s reading file", err)}fmt.Println(string(n))var student StudentunmarshalErr := json.Unmarshal(n, &student)if unmarshalErr != nil {fmt.Println("反序列化失败:", unmarshalErr)}studentinfo[student.Id] = student}fmt.Println("当前studentinfo的信息为:", studentinfo)
}func main() {studentinfo = make(map[int64]Student)var studentChan chan Student = make(chan Student)recoverInfo()go persistent()go appendPersistent(studentChan)for {var operator intfmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")fmt.Scanln(&operator)switch operator {case 1:add(studentChan)case 2:search()case 3:list()default:fmt.Println("输入不符合预期,不进行任何操作")}}
}
为了解决全量写的问题,咱们将实现方案改成了全量写+增量写的逻辑。如每隔10分钟再全量将数据写到磁盘,然后每次有数据新增都立马追加到磁盘,这样既避免了丢数据,也能保证内存中的数据跟磁盘中是一致的。接下来咱们照惯例继续演示一下
通过演示可以看到,每次新加的数据都会立马追加到文件中,同时咱们的定时任务也会周期性的将内存中的数据全量写一次到磁盘文件中。本次实现主要用了go的通道功能,在新增数据func跟追加写的func建立通道,每次有新加数据就通过通道传递给追加写的func,func收到后就将数据追加到磁盘文件中,避免了数据丢失的可能。小结如下
- 完成数据增量/全量持久化功能,拓展知识点:chan、文件追加写
6.版本六
上个版本咱们已经完成了功能的开发,但作为一名极客,咱们也要尽可能的对代码设计进行优化,因此这个版本最后对程序进行一版优化,优化后的代码如下
go">package mainimport ("bufio""encoding/json""flag""fmt""io""log""os""time"
)type Student struct {Id int64Name stringAge intChinese, Math, English float32 //类型想同的话可以这样简写
}var studentinfo map[int64]Studentfunc search() {var id int64fmt.Println("请录入要查询的学生学号:")fmt.Scanln(&id)student, ok := studentinfo[id]if ok == true {fmt.Println("查到该学生,此学生的详细信息为:", student)return}fmt.Println("未找到该学生信息")
}func add(studentChan chan<- Student) {var num intfmt.Println("请录入要录入的学生数量:")fmt.Scanln(&num)fmt.Println("请输入学生的学号,姓名,年龄,语文成绩,数学成绩,英语成绩,使用空格进行分隔:")for i := 0; i < num; i++ {student := new(Student)fmt.Scanf("%d %s %d %f %f %f", &student.Id, &student.Name, &student.Age, &student.Chinese, &student.Math, &student.English)studentinfo[student.Id] = *studentstudentChan <- *student}fmt.Println("所有学生成绩录入完毕")
}func list() {fmt.Println("所有学生信息如下\n", studentinfo)
}func persistent() {for {//定期将map的数据持久化到本地文件 student_info.txt上time.Sleep(1 * time.Minute)fmt.Println("========当前是持久化协程,准备进行数据持久化=========")f, err := os.Create("student_info.txt")if err != nil {fmt.Println(err)return}fmt.Println("持久化时查询到的学生信息studentinfo为:", studentinfo)var count intfor _, student := range studentinfo {count++res, err := json.Marshal(student)fmt.Println("序列化后的结果是:", string(res))if err != nil {fmt.Println(err)}fmt.Println(string(res))_, writeErr := f.WriteString(string(res) + "\n")if writeErr != nil {fmt.Println(err)f.Close()return}}fmt.Println("本次共持久化学生数量为:", count)fmt.Println(" written successfully")err = f.Close()if err != nil {fmt.Println(err)return}fmt.Println("========当前是持久化协程,成功完成数据持久化=========")}
}func innerAppendPersistent(student Student) {file, err := os.OpenFile("student_info.txt", os.O_WRONLY|os.O_APPEND, 0666)if err != nil {fmt.Println("文件打开失败", err)return}defer file.Close()res, err := json.Marshal(student)fmt.Println("序列化后的结果是:", string(res))if err != nil {fmt.Println(err)}//写入数据时使用带有缓冲的方式write := bufio.NewWriter(file)write.WriteString(string(res) + "\n")write.Flush()fmt.Println("当前学生信息追加成功", student)
}func appendPersistent(studentChan <-chan Student) {//增量持久化for {time.Sleep(250 * time.Millisecond)select {case student := <-studentChan:fmt.Println("接收到信息:", student)innerAppendPersistent(student)breakdefault://fmt.Println("没有接收到任何值")}}
}func recoverInfo() {//从持久化中进行数据恢复fptr := flag.String("fpath", "student_info.txt", "file path to read from")flag.Parse()f, err := os.Open(*fptr)if err != nil {log.Fatal(err)}defer func() {if err = f.Close(); err != nil {log.Fatal(err)}}()r := bufio.NewReader(f)for {n, _, err := r.ReadLine()if err == io.EOF {fmt.Println("finished reading file")break}if err != nil {fmt.Printf("Error %s reading file", err)}fmt.Println(string(n))var student StudentunmarshalErr := json.Unmarshal(n, &student)if unmarshalErr != nil {fmt.Println("反序列化失败:", unmarshalErr)}studentinfo[student.Id] = student}fmt.Println("当前studentinfo的信息为:", studentinfo)
}func main() {studentinfo = make(map[int64]Student)var studentChan chan Student = make(chan Student)recoverInfo()go persistent()go appendPersistent(studentChan)for {var operator intfmt.Println("请问要做什么:\n输入1 进行学生成绩录入,输入2进行学生成绩查询,输入3显示所有学生信息")fmt.Scanln(&operator)switch operator {case 1:add(studentChan)case 2:search()case 3:list()default:fmt.Println("输入不符合预期,不进行任何操作")}}
}
这个版本对代码做了进一步的优化,其中如chan改成单向的,结构体的初始化以及赋值等,到这里基本上就结束了,小结如下
- 完成代码优化,拓展知识点:单向chan、结构体初始化以及指针
四、总结与思考
通过上面一套组合拳打下来,相信你对go的基础知识以及设计系统都有一定的了解,如果可以希望可以基于自己的理解实现一个,因为实践才是检验真理的唯一标准。基于Go开发出学生成绩管理系统(或者其他系统)并不是终点,只是一个起点,准备好用这门优雅的语言去打开属于你的新世界吧~ 😋