上篇开篇介绍了一遍Golang 从0到1之任务提醒(开篇),这篇开始搭建项目,首先规划一下整体的目录。

目录就不过多解释了,这里并不复杂,主要想谈谈其他的点。
在做项目的时候,我不太喜欢上来就是干,我也不提倡这种方式。
最理想的方式应该是从设计做起。比如需求下来,大体先过一遍,从表设计开始做起,会涉及到哪些表,表与表之间的关系,当前的设计是否能满足未来扩展点需求......,画 ER 图也好,手写也罢,这是第一步。
然后具体落地到项目中会对应哪些模块,哪些类,需要定义类的哪些行为,类与类之间的交互关系,这又是一大块。
这样一圈下来你也大概知道这个需求是否存在坑,有坑的话可以及时进行沟通调整。最怕一上来就开干,快做完了发现有个大坑。
对应到我们这个项目,目前我们只需要一张保存任务的表即可。
CREATE TABLE `jobs` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`content` varchar(191) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '待办事项',`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`notice_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,`status` tinyint(3) unsigned NOT NULL DEFAULT '2' COMMENT '1已通知2待通知3失败',`phone` varchar(11) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '手机号码',`email` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '邮箱',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=46 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
配置文件
既然提到数据库,那么我们还需要初始化数据库行为。初始化数据库前,我们先得搞定配置。
在 conf 目录下创建 config.go 文件,
package configimport ("encoding/json""os")type Wechat struct {AppID stringAppSecret stringToken stringEncodingAESKey string}type Db struct {Address stringDbName stringUser stringPassword stringPort int}type Email struct {User stringPass stringHost stringPort int}type Configuration struct {Wechat *WechatDb *DbEmail *Email}var ConfAll *Configurationfunc LoadConfig() error {file, err := os.Open("config.json")if err != nil {return err}decoder := json.NewDecoder(file)ConfAll = &Configuration{}err = decoder.Decode(ConfAll)if err != nil {return err}return nil}
涉及到数据库配置、微信平台配置以及发送邮件配置信息。LoadConfig 就是配置项的初始化操作,赋值给变量 ConfAll,后续关于配置的信息就从这个变量取。
在 conf.json 文件中,设置对应的配置项值。只要别把这个文件上传到版本库就行。
{"Wechat": {"AppID": "xxx","AppSecret": "xxx","Token": "xxx","EncodingAESKey": "xxxxx"},"Db": {"Address": "127.0.0.1","DbName": "remind","User": "root","Password": "Passw0rd","Port": 3306},"Email": {"User": "[email protected]","Pass": "xxx","Host": "smtp.qq.com","Port": 25},}
连接池
接着开始初始化数据库操作。在 db 目录下创建文件 mysql.go。然后,
package dbimport ("database/sql""fmt""go-remind/config""time""gorm.io/driver/mysql""gorm.io/gorm")const (Url = "%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local&timeout=5s"MaxOpen = 5 // 最大打开数MaxIdle = 2 // 最大保留连接数LifeMinuteTime = 5 // 连接可重用最大时间)var Gorm *gorm.DBfunc InitDb(conf *config.Db) {var err errorvar sqlDb *sql.DBGorm, err = gorm.Open(mysql.Open(fmt.Sprintf(Url, conf.User, conf.Password,conf.Address, conf.Port, conf.DbName)), &gorm.Config{})if err != nil {fmt.Printf("open db:%v", err)}sqlDb, err = Gorm.DB()if err != nil {fmt.Printf("sql Db:%v", err)}// 允许最大并发打开连接数sqlDb.SetMaxOpenConns(MaxOpen)// 允许连接池中最多保留连接数sqlDb.SetConnMaxIdleTime(MaxIdle)// 允许连接可重用的最长时间sqlDb.SetConnMaxLifetime(LifeMinuteTime * time.Minute)}
这一段代码主要是初始化数据库,创建一个数据库连接池。
SetMaxOpenConns 允许最大并发打开连接数。SetConnMaxIdleTime 允许连接池中最多保留连接数。SetConnMaxLifetime 允许连接可重用的最长时间。
为什么需要使用数据库连接池?
从性能的角度上考虑,如果没有连接池,那么一个请求就创建一条与数据库的连接,然后操作完成事务提交,断开连接,下次请求重新创建连接。而连接必然需要经过 TCP 的三次握手,很大一部分取决于网络情况,这是一个耗时的过程。还有一点,如果系统层面不加以控制,在高并发的场景下,经常会出现数据库连接数超过最大值。
加了连接池,那么我们只需要在初始化的时候创建若干个预备连接放入池中,等到有需要的时候直接从池中拿出已有的连接和数据库进行交互,不必经过三次握手。等操作完成,再还回到连接池中,有助于提升系统的性能。
而且你也不必再去担心 To Many Connections。因为当应用程序发现连接池中没有可用的空闲连接时,应用程序将被迫进行等待,直到有新的空闲连接为止。
但是连接池也有不好的地方,比如当空闲连接过多,会导致资源大量的浪费。某种情况下空闲连接已关闭,但是没从连接池中移除,导致在使用的时候出现异常。
所以设置这些参数值成了一门学问。并没有标准的设置具体值的说法,只能根据具体的业务流量去加以测试判断。
接下来考虑有哪些操作。这个项目中会存在创建任务、获取即将执行通知的任务列表、发送成功或者失败修改对应任务状态,我们去完成这些基本操作。
数据操作
首先定义模型。在 models 下面创建一个 job.go 文件。
package modelsimport ("time")var (// 通知成功JobSuccess = 1// 待通知JobWait = 2// 通知失败JobFail = 3)type Job struct {Id int64Content stringCreatedAt time.TimeNoticeTime time.TimeStatus int8Phone stringEmail string}func (Job) TableName() string {return "jobs"}
在 logic 目录下也创建文件 job.go,这是真正和数据库交互的地方。
package logicimport ("fmt""go-remind/db""go-remind/models""time")type JobLogic struct{}func NewJob(content string, sendTime time.Time, phone, email string) *models.Job {return &models.Job{Content: content,NoticeTime: sendTime,Phone: phone,Email: email,}}// 插入任务func (j *JobLogic) Insert(job models.Job) error {fmt.Printf("值是:%v",db.Gorm)result := db.Gorm.Create(&job)return result.Error}// 根据时间获取近期要执行的任务列表func (j *JobLogic) GetJobsByTime(startTime string, endTime string) (jobs []models.Job, err error) {err = db.Gorm.Where("status=? and notice_time>=? and notice_time<=?", models.JobWait, startTime, endTime).Find(&jobs).Errorreturn}// 修改任务状态func (j *JobLogic) UpdateStatusById(id, status int) error {return db.Gorm.Where("id=?", id).Update("status", status).Error}
我们定义了一个 JobLogic 的结构体类型,JobLogic 提供了三个指针方法,分别用于用于创建任务、获取批量任务以及修改任务状态。
微信相关
我们是和微信公众号交互的,必然要接微信公众号消息回调。这一块有成熟的库,直接用就行,我用的是 silenceper/wechat。
在 handles 目录下创建 wechat.go,
package handlersimport ("fmt""github.com/gin-gonic/gin""github.com/silenceper/wechat/cache""github.com/silenceper/wechat/v2"offConfig "github.com/silenceper/wechat/v2/officialaccount/config""github.com/silenceper/wechat/v2/officialaccount/message". "go-remind/config")func Message(c *gin.Context) {defer func() {if err := recover(); err != nil {fmt.Printf("运行错误:%v", err)}}()//使用 memcache 保存access_token,也可选择redis或自定义cachewc := wechat.NewWechat()memory := cache.NewMemory()cfg := &offConfig.Config{AppID: ConfAll.Wechat.AppID,AppSecret: ConfAll.Wechat.AppSecret,Token: ConfAll.Wechat.Token,EncodingAESKey: ConfAll.Wechat.EncodingAESKey,Cache: memory,}officialAccount := wc.GetOfficialAccount(cfg)// 传入request和responseWriterserver := officialAccount.GetServer(c.Request, c.Writer)//设置接收消息的处理方法server.SetMessageHandler(func(msg message.MixMessage) *message.Reply {switch msg.MsgType {case message.MsgTypeText://回复消息:演示回复用户发送的消息res := message.NewText(msg.Content)//res := message.NewText(HandleMessage(msg.Content))return &message.Reply{MsgType: message.MsgTypeText, MsgData: res}case message.MsgTypeVoice:text := message.NewVoice(msg.Content)return &message.Reply{MsgType: message.MsgTypeText, MsgData: text}default:return &message.Reply{MsgType: message.MsgTypeText, MsgData: message.NewText("我睡着了,听不懂你在说啥")}}})//处理消息接收以及回复err := server.Serve()if err != nil {fmt.Println(err)return}//发送回复的消息server.Send()}
上面的逻辑主要是当用户在公众号发送对应信息,微信根据开发者配置回调地址,把信息订阅给你。由你来进行进一步的处理。当然了,目前业务上的信息提取工作我们还暂时没写,不急。
注意看最上面,这句话很眼熟吧。
defer func() {if err := recover(); err != nil {fmt.Printf("运行错误:%v", err)}}()
到这里,整体的基础工作做的差不多了,还需要给微信提供一个接口,并且把这些服务连接并运行,让程序跑起来。
在 main.go 下,
package mainimport ("github.com/gin-gonic/gin". "go-remind/config""go-remind/db""go-remind/handlers""log")func init() {// 初始化配置文件err := LoadConfig()if err != nil {log.Fatal("初始化错误:", err)}// 初始化数据库连接池if err = db.InitDb(ConfAll.Db); err != nil {log.Fatal("初始化错误:", err)}}func main() {r := gin.Default()// 开放一个路由接口r.GET("/msg", handlers.Message)_ = r.Run()}
很简单吧。虽然只需要提供一个接口,但是还是使用了 gin。不要在意这些。
go 相关的路由包很多,如果有特殊场景或者对性能敏感的话,就需要去好好调研各个包了。我用 gin 的原因是下一个项目会用到 gin,当然这是后话了。
最后我们来总结一下这一篇文章。主要完成了初始化配置文件、初始化数据库连接池,完成表的设计并且实现具体的业务操作。完成公众号的基础回调事件等操作,让我们继续。
另外这个项目我放在:https://github.com/wuqinqiang/go-remind 感兴趣可以 clone。
资料下载
点击下方卡片关注公众号,发送特定关键字获取对应精品资料!
-
回复「电子书」,获取入门、进阶 Go 语言必看书籍。
-
回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!
-
回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。
-
回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。
-
回复「后台」,获取后台开发必看 10 本书籍。
对了,看完文章,记得点击下方的卡片。关注我哦~ ???
如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!
文章评论