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

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

9f57046c3a7d18faa79445e9f486ebcd.png

目录就不过多解释了,这里并不复杂,主要想谈谈其他的点。

在做项目的时候,我不太喜欢上来就是干,我也不提倡这种方式。

最理想的方式应该是从设计做起。比如需求下来,大体先过一遍,从表设计开始做起,会涉及到哪些表,表与表之间的关系,当前的设计是否能满足未来扩展点需求......,画 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": "1185079673@qq.com",
    "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,非常感谢!664be7f24ed2ae34e714825cac225095.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值