5.9 Go语言项目实战:驾考系统

Amiee7 · 2019-03-18 09:10:34 · 4122 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2019-03-18 09:10:34 的主题,其中的信息可能已经有所发展或是发生改变。

本篇文章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
}

起名系统

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语言从入门到精通开发实战技能。


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

4122 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传