本篇文章IT兄弟连GO语言学院小美
给读者们分享一下5.9 Go语言项目实战:驾考系统
对GO语言感兴趣想要学习Golang开发技术的小伙伴就随小编来了解一下吧。
**项目需求**
- 考场签到,名字丢入管道;
- 只有5个车道,最多供5个人同时考试;
- 考生按签到顺序依次考试,给予考生10%的违规几率;
- 每3秒钟巡视一次,发现违规的清出考场,否则输出考场时序良好;
- 所有考试者考完后,向MySQL数据库录入考试成绩;
- 成绩录入完毕通知考生,考生查阅自己的成绩;
- 当前目录下的成绩录入MySQL数据库,数据库允许一写多读;
- 再次查询成绩使用Redis缓存(二级缓存);
- 整理优化代码,提高复用程度;
**主要技术栈**
- 管道并发
- MySQL-Redis二级缓存
- 通用的数据库工具的封装
- 类库封装和复用
**错误处理工具**
- 主要用于输出错误与错误爆发的场景,并暴力退出程序(当然是在DEBUG模式下)
```
package utils
import (
"fmt"
"os"
)
/*处理错误:有错误时暴力退出*/
func HandlerError(err error, when string) {
if err != nil {
fmt.Println(when, err)
os.Exit(1)
}
}
```
**数学工具**
- 这里主要封装了一个获取指定范围内随机数的方法
- 考虑到任意两次调用间时间种子不得相同,我们使用了强制同步和阻塞睡眠
```
package utils
import (
"time"
"math/rand"
"sync"
)
var(
//随机数互斥锁(确保GetRandomInt不能被并发访问)
randomMutex sync.Mutex
)
/*获取[start,end]范围内的随机数*/
func GetRandomInt(start, end int) int {
randomMutex.Lock()
<-time.After(1 * time.Nanosecond)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
n := start + r.Intn(end-start+1)
randomMutex.Unlock()
return n
}
```
**起名系统**
- 详见:[起名系统详解](https://blog.csdn.net/u010986776/article/details/84039389)
```
package utils
var (
//姓氏
familyNames = []string{"赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "楚", "卫", "蒋", "沈", "韩", "杨", "张", "欧阳", "东门", "西门", "上官", "诸葛", "司徒", "司空", "夏侯"}
//辈分(宗的永其光...)
middleNamesMap = map[string][]string{}
//名字
lastNames = []string{"春", "夏", "秋", "冬", "风", "霜", "雨", "雪", "木", "禾", "米", "竹", "山", "石", "田", "土", "福", "禄", "寿", "喜", "文", "武", "才", "华"}
)
/*初始化姓氏和对应的辈分*/
func init() {
for _, x := range familyNames {
if x != "欧阳" {
middleNamesMap[x] = []string{"德", "惟", "守", "世", "令", "子", "伯", "师", "希", "与", "孟", "由", "宜", "顺", "元", "允", "宗", "仲", "士", "不", "善", "汝", "崇", "必", "良", "友", "季", "同"}
} else {
middleNamesMap[x] = []string{"宗", "的", "永", "其", "光"}
}
}
}
/*获得随机姓名*/
func GetRandomName() (name string) {
familyName := familyNames[GetRandomInt(0, len(familyNames)-1)]
middleName := middleNamesMap[familyName][GetRandomInt(0, len(middleNamesMap[familyName])-1)]
lastName := lastNames[GetRandomInt(0, len(lastNames)-1)]
return familyName + middleName + lastName
}
```
**数据模型**
- 主要记录学生的姓名和考试成绩
```
package utils
/*考试成绩*/
type ExamScore struct {
Id int `db:"id"`
Name string `db:"name"`
Score int `db:"score"`
}
```
**数据库工具**
这份代码实现了二级缓存的核心功能,包括:
- 将全员考试成绩单(姓名/成绩键值对map)写入MySQL数据库
- 根据姓名从Redis缓存查询分数
- 将姓名与分数写入Redis缓存
- 还实现了一个通用的MySQL表查询方法:根据任意表名和查询条件map进行查询,并将结果送入指定的指针地址中,从而具有较高的复用价值
```
package utils
import (
"fmt"
/*MySQL*/
_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
"github.com/garyburd/redigo/redis"
"errors"
"sync"
)
var(
//数据库读写锁
dbMutext sync.RWMutex
)
/*
通用Mysql查询工具
tableName 要查询的表名
argsMap 查询条件集合
dest 查询结果存储地址
*/
func QueryFromMysql(tableName string,argsMap map[string]interface{},dest interface{}) (err error) {
fmt.Println("QueryScoreFromMysql...")
//写入期间不能进行数据库读访问
dbMutext.RLock()
db, err := sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")
HandlerError(err, `sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")`)
defer db.Close()
selection := ""
values := make([]interface{}, 0)
for col,value := range argsMap{
selection += (" and "+col+"=?")
values = append(values, value)
}
selection = selection[4:]
sql := "select * from "+tableName+" where "+selection;
err = db.Select(dest, sql, values...)
if err != nil {
fmt.Println(err, `db.Select(&examScores, "select * from score where name=?;", name)`)
return
}
dbMutext.RUnlock()
return nil
}
/*将全员考试成绩单写入MySQL数据库*/
func WriteScore2Mysql(scoreMap map[string]int) {
//锁定为写模式,写入期间不允许读访问
dbMutext.Lock()
db, err := sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")
HandlerError(err, `sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")`)
defer db.Close()
for name, score := range scoreMap {
_, err := db.Exec("insert into score(name,score) values(?,?);", name, score)
HandlerError(err, `db.Exec("insert into score(name,score) values(?,?);", name, score)`)
fmt.Println("插入成功!")
}
fmt.Println("成绩录入完毕!")
//解锁数据库,开放查询
dbMutext.Unlock()
}
/*根据姓名从Redis缓存查询分数*/
func QueryScoreFromRedis(name string) (score int, err error) {
fmt.Println("QueryScoreFromRedis...")
conn, err := redis.Dial("tcp", "localhost:6379")
HandlerError(err, `redis.Dial("tcp", "local:6379")`)
defer conn.Close()
reply, e := conn.Do("get", name)
if reply != nil {
score, e = redis.Int(reply, e)
//fmt.Println("!!!!!!!!!!!!", score, e)
} else {
return 0, errors.New("未能从Redis中查到数据")
}
if err != nil {
fmt.Println(err, `conn.Do("get", name)或者redis.Int(reply, err)`)
return 0, e
}
return score, nil
}
/*将姓名与分数写入Redis缓存*/
func WriteScore2Redis(name string, score int) error {
conn, err := redis.Dial("tcp", "localhost:6379")
HandlerError(err, `redis.Dial("tcp", "local:6379")`)
defer conn.Close()
_, err = conn.Do("set", name, score)
fmt.Println("Redis写入成功!")
return err
}
```
**考试业务封装**
- 这里封装了巡考、考试、查成绩逻辑
```
package main
import (
"time"
"fmt"
"utils"
)
var(
chNames = make(chan string, 100)
examers = make([]string, 0)
//信号量,只有5条车道
chLanes = make(chan int, 5)
//违纪者
chFouls = make(chan string, 100)
//考试成绩
scoreMap = make(map[string]int)
)
/*巡考逻辑*/
func Patrol() {
ticker := time.NewTicker(1 * time.Second)
for {
//fmt.Println("战狼正在巡考...")
select {
case name := <-chFouls:
fmt.Println(name, "考试违纪!!!!! ")
default:
fmt.Println("考场秩序良好")
}
<-ticker.C
}
}
/*考试逻辑*/
func TakeExam(name string) {
chLanes <- 123
fmt.Println(name, "正在考试...")
//记录参与考试的考生姓名
examers = append(examers, name)
//生成考试成绩
score := utils.GetRandomInt(0, 100)
//fmt.Println(score)
scoreMap[name] = score
if score < 10 {
score = 0
chFouls <- name
//fmt.Println(name, "考试违纪!!!", score)
}
//考试持续5秒
<-time.After(400 * time.Millisecond)
<-chLanes
//wg.Done()
}
/*二级缓存查询成绩*/
func QueryScore(name string) {
score, err := utils.QueryScoreFromRedis(name)
if err != nil {
fmt.Println(err)
//score, _ = utils.QueryScoreFromMysql(name)
scores := make([]utils.ExamScore, 0)
argsMap := make(map[string]interface{})
argsMap["name"] = name
//argsMap["score"] = 50
err = utils.QueryFromMysql("score", argsMap, &scores)
utils.HandlerError(err,`utils.QueryFromMysql("score", argsMap, &scores)`)
fmt.Println("Mysql成绩:", name, ":", scores[0].Score)
/*将数据写入Redis*/
utils.WriteScore2Redis(name, scores[0].Score)
} else {
fmt.Println("Redis成绩:", name, ":", score)
}
//wg.Done()
}
```
**主调程序**
```
package main
import (
"time"
"fmt"
"utils"
"sync"
)
/*
考场签到,名字丢入管道;
只有5个车道,最多供5个人同时考试;
考生按签到顺序依次考试,给予考生10%的违规几率;
每3秒钟巡视一次,发现违规的清出考场,否则输出考场时序良好;
所有考试者考完后,向MySQL数据库录入考试成绩;
成绩录入完毕通知考生,考生查阅自己的成绩;
当前目录下的成绩录入MySQL数据库,数据库允许一写多读;
再次查询成绩使用Redis缓存(二级缓存);
整理优化代码,提高复用程度;
*/
var (
wg sync.WaitGroup
)
/*主程序*/
func main() {
for i := 0; i < 20; i++ {
chNames <- utils.GetRandomName()
}
close(chNames)
/*巡考*/
go Patrol()
/*考生并发考试*/
for name := range chNames {
wg.Add(1)
go func(name string) {
TakeExam(name)
wg.Done()
}(name)
}
wg.Wait()
fmt.Println("考试完毕!")
/*录入成绩*/
wg.Add(1)
go func() {
utils.WriteScore2Mysql(scoreMap)
wg.Done()
}()
//故意给一个时间间隔,确保WriteScore2DB先抢到数据库的读写锁
<-time.After(1 * time.Second)
/*考生查询成绩*/
for _, name := range examers {
wg.Add(1)
go func(name string) {
QueryScore(name)
wg.Done()
}(name)
}
<-time.After(1 * time.Second)
for _, name := range examers {
wg.Add(1)
go func(name string) {
QueryScore(name)
wg.Done()
}(name)
}
wg.Wait()
fmt.Println("END")
}
```
想要了解更多关于GO语言开发方面内容的小伙伴,
请关注IT兄弟连官网、公众号:GO语言研习社,
IT兄弟连教育有专业的微软、谷歌讲师为您指导,
此外IT兄弟连老师精心推出的GO语言教程定能让你快速掌握GO语言从入门到精通开发实战技能。
有疑问加站长微信联系(非本文作者)