上篇开篇介绍了一遍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 语言必看书籍。
-
回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!
-
回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。
-
回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。
-
回复「后台」,获取后台开发必看 10 本书籍。
对了,看完文章,记得点击下方的卡片。关注我哦~ ???
如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!
文章评论