天天看點

還不會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背景開發時,代碼都是有分層的,比較熟悉的有​

​dao​

​​、​

​service​

​​、​

​controller​

​​、​

​model​

​​等等。其實​

​dao​

​​、​

​service​

​​、​

​controller​

​​,是有調用先後順序的。​

​controller​

​​調用​

​service​

​​層,​

​service​

​​層調用​

​dao​

​​層,這就形成了依賴關系,我們在實際開發中,通過分層依賴注入的方式,更加層次分明,且代碼是易于維護的。是以,我寫了一個樣例,讓我們來學習一下怎麼使用。這個采用​

​cron​

​​定時任務代替​

​controller​

​​來代替​

​controller​

​​,​

​cron​

​定時任務我會在後文進行講解。

//+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.DB​

​​,​

​service.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​

​ 個域,每一個域代表一個時間含義。 格式如下:

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

[年]的部分通常是可以省略的,實際上由前六部分組成。

關于各部分的定義,我們以一個表格的形式呈現:

是否必填 值以及範圍 通配符
0-59 , - * /
0-59 , - * /
0-23 , - * /
1-31 , - * ? / L W
1-12 或 JAN-DEC , - * /
1-7 或 SUN-SAT , - * ? / L #
1970-2099 , - * /

看這個值的範圍,還是很好了解的,最難了解的是通配符,我們着重來講一下通配符。

  • ​,​

    ​​ 這裡指的是在兩個以上的時間點中都執行,如果我們在 “分” 這個域中定義為 ​

    ​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,可自行下載下傳學習:https://github.com/asong2020/Golang_Dream/tree/master/wire_cron_example

總結