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

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

这篇开始搭建项目,首先规划一下整体的目录。

1668491789546.jpg

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

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

最理想的方式应该是从设计做起。比如需求下来,大体先过一遍,从表设计开始做起,会涉及到哪些表,表与表之间的关系,当前的设计是否能满足未来扩展点需求......,画 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 数据库
Golang 从0到1之任务提醒(二)
Golang 从0到1之任务提醒(二)
155 0
|
3月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
140 4
Golang语言之管道channel快速入门篇
|
3月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
71 4
Golang语言文件操作快速入门篇
|
3月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
112 3
Golang语言之gRPC程序设计示例
|
3月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
100 4
|
3月前
|
Go
Golang语言错误处理机制
这篇文章是关于Golang语言错误处理机制的教程,介绍了使用defer结合recover捕获错误、基于errors.New自定义错误以及使用panic抛出自定义错误的方法。
55 3
|
3月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
72 4
Golang语言goroutine协程篇
|
3月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
77 3
Golang语言之Prometheus的日志模块使用案例
|
3月前
|
Go
Golang语言之函数(func)进阶篇
这篇文章是关于Golang语言中函数高级用法的教程,涵盖了初始化函数、匿名函数、闭包函数、高阶函数、defer关键字以及系统函数的使用和案例。
71 3
Golang语言之函数(func)进阶篇
|
2月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
38 0