学会wire依赖注入、cron定时任务其实就是这么简单

前言

嗨,我小asong又回来了。托了两周没有更新,最近比较忙,再加上自己懒,所以嘛,嗯嗯,你们懂的。不过我今天的带来的分享,绝对干货,在实际项目中开发也是需要用到的,所以为了能够讲明白,我特意写了一个样例,仅供参考。本文会围绕样例进行展开学习,已上传github,可自行下载。好了,不说废话了,知道你们迫不及待了,我们直接开始吧!!!

wire

依赖注入

在介绍wire之前,我们先来了解一下什么是依赖注入。使用过Spring的同学对这个应该不会陌生。其中控制反转(IOC)最常见的方式就叫做依赖注入。将依赖的类作为行参放入依赖中的类就成为依赖注入。这么说可能你们不太懂。用一句大白话来说,一个实例化的对象,本来我接受各种参数来构造一个对象,现在只接受一个参数,对对象的依赖是注入进来的,和它的构造方式解耦了。构造他这个控制操作也交给了第三方,即控制反转。举个例子:go中是没有类的概念的,以结构体的形式体现。假设我们现在船类,有个浆类,我们现在想要设置该船有12个浆,那我们可以写出如下代码:

package main

import (
 "fmt"
)

type ship struct {
 pulp *pulp
}
func NewShip(pulp *pulp) *ship{
 return &ship{
  pulp: pulp,
 }
}
type pulp struct {
 count int
}

func Newpulp(count int) *pulp{
 return &pulp{
  count: count,
 }
}

func main(){
 p:= Newpulp(12)
 s := NewShip(p)
 fmt.Println(s.pulp.count)
}


相信你们一眼就看出问题了,每当需求变动时,我们都要重新创建一个对象来指定船桨,这样的代码不易维护,我们变通一下。

package main

import (
 "fmt"
)

type ship struct {
 pulp *pulp
}
func NewShip(pulp *pulp) *ship{
 return &ship{
  pulp: pulp,
 }
}
type pulp struct {
 count int
}

func Newpulp() *pulp{
 return &pulp{
 }
}

func (c *pulp)set(count int)  {
 c.count = count
}

func (c *pulp)get() int {
 return c.count
}

func main(){
 p:= Newpulp()
 s := NewShip(p)
 s.pulp.set(12)
 fmt.Println(s.pulp.get())
}

这个代码的好处就在于代码松耦合,易维护,还易测试。如果我们现在更换需求了,需要20个船桨,直接s.pulp.set(20)就可以了。

wire的使用

wire有两个基础概念,Provider(构造器)和Injector(注入器)。Provider实际上就是创建函数,大家意会一下。我们上面InitializeCron就是Injector。每个注入器实际上就是一个对象的创建和初始化函数。在这个函数中,我们只需要告诉wire要创建什么类型的对象,这个类型的依赖,wire工具会为我们生成一个函数完成对象的创建和初始化工作。

拉了这么长,就是为了引出wire,上面的代码虽然是实现了依赖注入,这是在代码量少,结构不复杂的情况下,我们自己来实现依赖是没有问题的,当结构之间的关系变得非常复杂的时候,这时候手动创建依赖,然后将他们组装起来就会变的异常繁琐,并且很容出错。所以wire的作用就来了。在使用之前我们先来安装一下wire。

$ go get github.com/google/wire/cmd/wire

执行该命令会在$GOPATH/bin中生成一个可执行程序wire,这个就是代码生成器。别忘了吧$GOPATH/bin加入系统环境变量$PATH中。

先根据上面的简单例子,我们先来看看wire怎么用。我们先创建一个wire文件,文件内容如下:

//+build wireinject

package main

import (
 "github.com/google/wire"
)

type Ship struct {
 Pulp *Pulp
}
func NewShip(pulp *Pulp) *Ship {
 return &Ship{
  pulp: pulp,
 }
}
type Pulp struct {
 Count int
}

func NewPulp() *Pulp {
 return &Pulp{
 }
}

func (c *Pulp)set(count int)  {
 c.count = count
}

func (c *Pulp)get() int {
 return c.count
}

func InitShip() *Ship {
 wire.Build(
  NewPulp,
  NewShip,
  )
 return &Ship{}
}

func main(){

}

其中InitShip这个函数的返回值就是我们需要创建的对象类型,wire只需要知道类型,返回什么不重要。在函数中我们调用了wire.Build()将创建ship所依赖的的类型构造器传进去。这样我们就编写好了,现在我们需要到控制台执行wire

$ wire
wire: asong.cloud/Golang_Dream/wire_cron_example/ship: wrote /Users/asong/go/src/asong.cloud/Golang_Dream/wire_cron_example/ship/wire_gen.go

我们看到生成了wire_gen.go这个文件:

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from mian.go:

func InitShip() *Ship {
 pulp := NewPulp()
 ship := NewShip(pulp)
 return ship
}

// mian.go:

type Ship struct {
 pulp *Pulp
}

func NewShip(pulp *Pulp) *Ship {
 return &Ship{
  pulp: pulp,
 }
}

type Pulp struct {
 count int
}

func NewPulp() *Pulp {
 return &Pulp{}
}

func (c *Pulp) set(count int) {
 c.count = count
}

func (c *Pulp) get() int {
 return c.count
}

func main() {

}

可以看出来,生成的这个文件根据刚才定义的生成了InitShip()这个函数,依赖绑定关系也都实现了,我们直接调用这个函数,就可以了,省去了大量代码自己去实现依赖绑定关系。

**注意:**如果你是第一次使用wire,那么你一定会遇到一个问题,生成的代码和原来的代码会出现冲突,因为都定义相同的函数func InitShip() *Ship,所以这里需要在原文件中首行添加//+build wireinject,并且还要与包名有空行,这样就解决了冲突。

上面的例子还算是简单,下面我们来看一个比较多一点的例子,我们在日常web后台开发时,代码都是有分层的,比较熟悉的有daoservicecontrollermodel等等。其实daoservicecontroller,是有调用先后顺序的。controller调用service层,service层调用dao层,这就形成了依赖关系,我们在实际开发中,通过分层依赖注入的方式,更加层次分明,且代码是易于维护的。所以,我写了一个样例,让我们来学习一下怎么使用。这个采用cron定时任务代替controller来代替controllercron定时任务我会在后文进行讲解。

//+build wireinject

package wire

import (
 "github.com/google/wire"

 "asong.cloud/Golang_Dream/wire_cron_example/config"
 "asong.cloud/Golang_Dream/wire_cron_example/cron"
 "asong.cloud/Golang_Dream/wire_cron_example/cron/task"
 "asong.cloud/Golang_Dream/wire_cron_example/dao"
 "asong.cloud/Golang_Dream/wire_cron_example/service"
)

func InitializeCron(mysql *config.Mysql)  *cron.Cron{
 wire.Build(
  dao.NewClientDB,
  dao.NewUserDB,
  service.NewUserService,
  task.NewScanner,
  cron.NewCron,
  )
 return &cron.Cron{}
}

我们来看看这段代码,dao.NewClientDB即创建一个*sql.DB对象,依赖于mysql的配置文件,dao.NewUserDB即创建一个*UserDB对象,他依赖于*sql.DBservice.NewUserService即创建一个UserService对象,依赖于*UserDB对象,task.NewScanner创建一个*Scanner对象,他依赖于*UserService对象,cron。NewCron创建一个*Cron对象,他依赖于*Scanner对象,其实这里是层层绑定关系,一层调一层,层次分明,且易于代码维护。

好啦,基本使用就介绍到这里,我们接下来我们学习一下cron

cron

基础学习

我们在日常开发或运维中,经常遇到一些周期性执行的任务或需求,例如:每一段时间执行一个脚本,每个月执行一个操作。linux给我们提供了一个便捷的方式—— crontab定时任务;crontab就是一个自定义定时器,我们可以利用 crontab 命令在固定的间隔时间执行指定的系统指令或 shell script 脚本。而这个时间间隔的写法与我们平常用到的cron 表达式相似。作用都是通过利用字符或命令去设置定时周期性地执行一些操作.

知道了基本概念,我们就来介绍一下cron表达式。常用的cron规范格式有两种:一种是“标准”cron格式,由cron linux系统程序使用,还有一种是Quartz Scheduler使用cron格式。这两种的差别就在一个是支持seconds字段的,一个是不支持的,不过差距不是很大,我们接下来的讲解都带上seconds这个字段,没有影响的。

cron 表达式是一个字符串,该字符串由 6 个空格分为 7 个域,每一个域代表一个时间含义。 格式如下:

[秒] [分] [时] [日] [月] [周] [年]

[年]的部分通常是可以省略的,实际上由前六部分组成。

关于各部分的定义,我们以一个表格的形式呈现:

ble data-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">

看这个值的范围,还是很好理解的,最难理解的是通配符,我们着重来讲一下通配符。

  • , 这里指的是在两个以上的时间点中都执行,如果我们在 “分” 这个域中定义为 5,10,15 ,则表示分别在第5分,第10分 第15分执行该定时任务。
  • - 这个比较好理解就是指定在某个域的连续范围,如果我们在 “时” 这个域中定义 6-12,则表示在6到12点之间每小时都触发一次,用 , 表示 6,7,8,9,10,11,12
  • * 表示所有值,可解读为 “每”。 如果在“日”这个域中设置 *,表示每一天都会触发。
  • ? 表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的8号触发一个操作,但不关心是周几,我们可以这么设置 0 0 0 8 * ?
  • / 在某个域上周期性触发,该符号将其所在域中的表达式分为两个部分,其中第一部分是起始值,除了秒以外都会降低一个单位,比如 在 “秒” 上定义 5/10 表示从 第 5 秒开始 每 10 秒执行一次,而在 “分” 上则表示从 第 5 秒开始 每 10 分钟执行一次。
  • L 表示英文中的LAST 的意思,只能在 “日”和“周”中使用。在“日”中设置,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年), 在“周”上表示周六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在“周”上设置”7L”这样的格式,则表示“本月最后一个周六”
  • W 表示离指定日期的最近那个工作日(周一至周五)触发,只能在 “日” 中使用且只能用在具体的数字之后。若在“日”上置”15W”,表示离每月15号最近的那个工作日触发。假如15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果是 “1W” 就只能往本月的下一个最近的工作日推不能跨月往上一个月推。
  • # 表示每月的第几个周几,只能作用于 “周” 上。例如 ”2#3” 表示在每月的第三个周二。

学习了通配符,下面我们来看几个例子:

  • 每天10点执行一次:0 0 10 * * *
  • 每隔10分钟执行一次:0 */10 * * *
  • 每月1号凌晨3点执行一次:0 0 3 1 * ?
  • 每月最后一天23点30分执行一次:0 30 23 L * ?
  • 每周周六凌晨3点实行一次:0 0 3 ? * L
  • 在30分、50分执行一次:0 30,50 * * * ?

go中使用cron

前面我们学习了基础,现在我们想要在go项目中使用定时任务,我们该怎么做呢?github上有一个星星比较高的一个cron库,我们可以使用robfig/cron这个库开发我们的定时任务。

学习之前,我们先来安装一下cron

$ go get -u github.com/robfig/cron/v3

这是目前比较稳定的版本,现在这个版本是采用标准规范的,默认是不带seconds,如果想要带上字段,我们需要创建cron对象是去指定。一会展示。我们先来看一个简单的使用:

package main

import (
  "fmt"
  "time"

  "github.com/robfig/cron/v3"
)

func main() {
  c := cron.New()

  c.AddFunc("@every 1s", func() {
    fmt.Println("task start in 1 seconds")
  })

  c.Start()
  select{}
}

这里我们使用cron.New创建一个cron对象,用于管理定时任务。调用cron对象的AddFunc()方法向管理器中添加定时任务。AddFunc()接受两个参数,参数 1 以字符串形式指定触发时间规则,参数 2 是一个无参的函数,每次触发时调用。@every 1s表示每秒触发一次,@every后加一个时间间隔,表示每隔多长时间触发一次。例如@every 1h表示每小时触发一次,@every 1m2s表示每隔 1 分 2 秒触发一次。time.ParseDuration()支持的格式都可以用在这里。调用c.Start()启动定时循环。

注意一点,因为c.Start()启动一个新的 goroutine 做循环检测,我们在代码最后加了一行select{}防止主 goroutine 退出。

上面我们定义时间时,使用的是cron预定义的时间规则,那我们就学习一下他都有哪些预定义的一些时间规则:

  • @yearly:也可以写作@annually,表示每年第一天的 0 点。等价于0 0 1 1 *
  • @monthly:表示每月第一天的 0 点。等价于0 0 1 * *
  • @weekly:表示每周第一天的 0 点,注意第一天为周日,即周六结束,周日开始的那个 0 点。等价于0 0 * * 0
  • @daily:也可以写作@midnight,表示每天 0 点。等价于0 0 * * *
  • @hourly:表示每小时的开始。等价于0 * * * *

cron也是支持固定时间间隔的,格式如下:

@every <duration>

含义为每隔duration触发一次。<duration>会调用time.ParseDuration()函数解析,所以ParseDuration支持的格式都可以。

项目使用

因为我自己写的项目是通过实现job接口来加入定时任务,所以下面我们再来介绍一下Job接口的使用,除了直接将无参函数作为回调外,cron还支持job接口:

type Job interface{
 Run()
}

我们需要实现这个接口,这里我就以我写的例子来做演示吧,我现在这个定时任务是周期扫DB表中的数据,实现任务如下:

package task

import (
 "fmt"

 "asong.cloud/Golang_Dream/wire_cron_example/service"
)

type Scanner struct {
 lastID uint64
 user *service.UserService
}

const  (
 ScannerSize = 10
)

func NewScanner(user *service.UserService)  *Scanner{
 return &Scanner{
  user: user,
 }
}

func (s *Scanner)Run()  {
 err := s.scannerDB()
 if err != nil{
  fmt.Errorf(err.Error())
 }
}

func (s *Scanner)scannerDB()  error{
 s.reset()
 flag := false
 for {
  users,err:=s.user.MGet(s.lastID,ScannerSize)
  if err != nil{
   return err
  }
  if len(users) < ScannerSize{
   flag = true
  }
  s.lastID = users[len(users) - 1].ID
  for k,v := range users{
   fmt.Println(k,v)
  }
  if flag{
   return nil
  }
 }
}

func (s *Scanner)reset()  {
 s.lastID = 0
}

上面是实现Run方法的部分,之后我们还需要调用cron对象的AddJob方法将Scanner对象添加到定时管理器中。

package cron

import (
 "github.com/robfig/cron/v3"

 "asong.cloud/Golang_Dream/wire_cron_example/cron/task"
)

type Cron struct {
 Scanner *task.Scanner
 Schedule *cron.Cron
}

func NewCron(scanner *task.Scanner) *Cron {
 return &Cron{
  Scanner: scanner,
  Schedule: cron.New(),
 }
}

func (s *Cron)Start()  error{
 _,err := s.Schedule.AddJob("*/1 * * * *",s.Scanner)
 if err != nil{
  return err
 }
 s.Schedule.Start()
 return nil
}

实际上AddFunc()方法内部也调用了AddJob()方法。首先,cron基于func()类型定义一个新的类型FuncJob

// cron.go
type FuncJob func()

然后让FuncJob实现Job接口:

// cron.go
func (f FuncJob) Run() {
  f()
}

AddFunc()方法中,将传入的回调转为FuncJob类型,然后调用AddJob()方法:

func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) {
  return c.AddJob(spec, FuncJob(cmd))
}

好啦,基本的使用到这里我们就讲解完了,最后再补充一下最后一个知识点,也就时间规范的问题,默认v3版本是不带seconds字段的,要想使用需要这样使用

cron.New(cron.WithSeconds())

创建对象的传入这个参数就可以了。

好啦。我想要讲解的完事了,代码就不运行了,已上传github,可自行下载学习:github.com/asong2020/Go

总结

今天的文章就到这里了,这一篇总结的并不全,只是达到入门的一个效果,想要继续深入,还需要各位小伙伴自行看文档学习呦。学会看官方文档,才能进步更多的呦。就比如时间规范这里,如果不看文档,我就不会知道现在使用的时间规范是什么的,所以还是要养成看文档的好习惯。打个预告,下一期是go-elastic的教程,有需要的小伙伴可以关注一下。

我是asong,一名普普通通的程序猿,让我一起慢慢变强吧。欢迎各位的关注,我们下期见~~~

推荐往期文章:

编辑于 2020-09-08 22:36