Golang 从0到1之任务提醒(一)

2022年4月26日 316点热度 0人点赞 0条评论

上篇开篇介绍了一遍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 config
import ( "encoding/json" "os")
type Wechat struct { AppID string AppSecret string Token string EncodingAESKey string}
type Db struct { Address string DbName string User string Password string Port int}
type Email struct { User string Pass string Host string Port int}
type Configuration struct { Wechat *Wechat Db *Db Email *Email}
var ConfAll *Configuration
func 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 db
import ( "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.DB
func InitDb(conf *config.Db) { var err error var sqlDb *sql.DB Gorm, 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 models
import ( "time")
var ( // 通知成功 JobSuccess = 1 // 待通知 JobWait = 2 // 通知失败 JobFail = 3)
type Job struct { Id int64 Content string CreatedAt time.Time NoticeTime time.Time Status int8 Phone string Email string}
func (Job) TableName() string { return "jobs"}

在 logic 目录下也创建文件 job.go,这是真正和数据库交互的地方。

package logic
import ( "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).Error return}// 修改任务状态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 handlers
import ( "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或自定义cache  wc := 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和responseWriter server := 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 main
import ( "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中,你犯过这些错误吗

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ ???

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!图片

9330Golang 从0到1之任务提醒(一)

root

这个人很懒,什么都没留下

文章评论