前言
嗨,我小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
-
表示英文中的LAST 的意思,隻能在 “日”和“周”中使用。在“日”中設定,表示當月的最後一天(依據目前月份,如果是二月還會依據是否是潤年), 在“周”上表示周六,相當于”7”或”SAT”。如果在”L”前加上數字,則表示該資料的最後一個。例如在“周”上設定”7L”這樣的格式,則表示“本月最後一個周六”L
-
表示離指定日期的最近那個工作日(周一至周五)觸發,隻能在 “日” 中使用且隻能用在具體的數字之後。若在“日”上置”15W”,表示離每月15号最近的那個工作日觸發。假如15号正好是周六,則找最近的周五(14号)觸發, 如果15号是周未,則找最近的下周一(16号)觸發.如果15号正好在工作日(周一至周五),則就在該天觸發。如果是 “1W” 就隻能往本月的下一個最近的工作日推不能跨月往上一個月推。W
-
表示每月的第幾個周幾,隻能作用于 “周” 上。例如 ”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
,表示每年第一天的 0 點。等價于@annually
;0 0 1 1 *
-
:表示每月第一天的 0 點。等價于@monthly
;0 0 1 * *
-
:表示每周第一天的 0 點,注意第一天為周日,即周六結束,周日開始的那個 0 點。等價于@weekly
;0 0 * * 0
-
:也可以寫作@daily
,表示每天 0 點。等價于@midnight
;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