博客 / 詳情

返回

Glacier Framework - 支持依賴注入的 Go 應用開發框架

其實這個項目在 4 年前就已經開始了,因為所有的功能都是基於日常工作中的需求來的,斷斷續續的補充和完善功能,之前都是在自己公司這邊的各種 Go 項目和我開源的一些項目中使用。很早之前就想把它開源出來,但是一直懶得寫文檔(感覺寫文檔是最難得事兒了),所以一直讓它靜靜地躺 Github 。今天終於補充了個簡版的文檔,是時候把它拿出來了😀。

感興趣的朋友們歡迎來看看啊,有磚拍磚,有需求提需求,一定虛心向大家學習!

Glacier 是一款支持依賴注入的模塊化的應用開發框架,它以 go-ioc 依賴注入容器核心,為 Go 應用開發解決了依賴傳遞和模塊化的問題。

  • 特性
  • 使用
  • 執行流程
  • 核心概念

    • 依賴注入

      • Binder
      • Resolver
    • Provider

      • ProviderBoot
      • DaemonProvider
      • ProviderAggregate
      • Service
      • ModuleLoadPolicy
      • Priority
  • Web 框架

    • Usage
    • 控制器
  • 事件管理

    • 本地內存作為事件存儲後端
    • Redis 作為事件存儲後端
  • 定時任務
  • 日誌
  • Eloquent ORM
  • 平滑退出
  • 第三方框架集成
  • 示例項目

特性

  • 依賴注入:通過依賴注入的方式來管理對象的依賴,支持單例、原型對象創建
  • 模塊化:通過 Provider 特性,輕鬆實現應用的模塊化
  • 內置 Web 開發支持:Glacier 內置了對 Web 應用開發的支持,提供了功能豐富的 API 簡化 web 開發

使用

創建一個新的項目,使用下面的命令安裝 Glacier 開發框架

go get github.com/mylxsw/glacier

為了簡化應用的創建過程,我們一般可以通過 starter 模板來創建應用

import "github.com/mylxsw/glacier/starter/app"
...

// 方法一:快捷啓動應用
app.MustStart("1.0", 3, func(app *app.App) error {
    // 這裏完成應用的初始化
    // ...
    return nil
})

// 方法二: 分步驟啓動應用
ins := app.Create("1.0", 3)
// 應用初始化
// ...
app.MustRun(ins)

示例:

app.MustStart("1.0", 3, func(ins *app.App) error {
    ins.AddStringFlag("listen", ":8080", "http listen address")
    
    ins.Provider(web.Provider(
        listener.FlagContext("listen"),
        web.SetRouteHandlerOption(func(cc infra.Resolver, router web.Router, mw web.RequestMiddleware) {
            router.Get("/", func(ctx web.Context) web.Response {
                return ctx.JSON(web.M{})
            })
        }),
    ))
    return nil
})
代碼示例可以參考當前項目的 example 目錄。

執行流程

執行流程

核心概念

依賴注入

Glacier 框架充分利用了 go-ioc 提供的依賴注入能力,為應用提供了功能強大的依賴注入特性。

在使用依賴注入特性時,首先要理解以下兩個接口的作用

  • infra.Binder 該接口用於對象創建實例方法的綁定,簡單説就是向 go-ioc 容器註冊對象的創建方法
  • infra.Resolver 該接口用於對象的實例化,獲取對象實例

無論是 Binder 還是 Resolver,都會有一個 interface{} 類型的參數,它的類型為符合一定規則的函數,後面在 BinderResolver 部分將會詳細説明。

Binder

infra.Binder 是一個對象定義接口,用於將實例的創建方法綁定到依賴注入容器,提供了以下常用方法

  • Prototype(initialize interface{}) error 原型綁定,每次訪問綁定的實例都會基於 initialize 函數重新創建新的實例
  • Singleton(initialize interface{}) error 單例綁定,每次訪問綁定的實例都是同一個,只會在第一次訪問的時候創建初始實例
  • BindValue(key string, value interface{}) error 將一個具體的值綁定到 key

PrototypeSingleton 方法參數 initialize interface{} 支持以下兩種形式

  • 形式1:func(依賴參數列表...) (綁定類型定義, error)

    // 這裏使用單例方法定義了數據庫連接對象的創建方法
    binder.Singleton(func(conf *Config) (*sql.DB, error) {
        return sql.Open("mysql", conf.MySQLURI)
    })
    
    binder.Singleton(func(c infra.FlagContext) *Config {
          ...
          return &Config{
              Listen:   c.String("listen"),
              MySQLURI: c.String("mysql_uri"),
              APIToken: c.String("api_token"),
              ...        
          }
      })
  • 形式2:func(注入參數列表...) 綁定類型定義

    binder.Singleton(func() UserRepo { return &userRepoImpl{} })
    binder.Singleton(func(db *sql.DB) UserRepo { 
        // 這裏我們創建的 userRepoImpl 對象,依賴 sql.DB 對象,只需要在函數
        // 參數中,將依賴列舉出來,容器會自動完成這些對象的創建
        return &userRepoImpl{db: db} 
    })

Resolver

infra.Resolver 是對象實例化接口,通過依賴注入的方式獲取實例,提供了以下常用方法

  • Resolve(callback interface{}) error 執行 callback 函數,自動為 callback 函數提供所需參數
  • Call(callback interface{}) ([]interface{}, error) 執行 callback 函數,自動為 callback 函數提供所需參數,支持返回值,返回參數為 Call 的第一個數組參數
  • AutoWire(object interface{}) error 自動對結構體對象進行依賴注入,object 必須是結構體對象的指針。自動注入字段(公開和私有均支持)需要添加 autowire tag,支持以下兩種

    • autowire:"@" 根據字段的類型來注入
    • autowire:"自定義key" 根據自定義的key來注入(查找名為 key 的綁定)
  • Get(key interface{}) (interface{}, error) 直接通過 key 來查找對應的對象實例
// Resolve
resolver.Resolve(func(db *sql.DB) {...})
err := resolver.Resolve(func(db *sql.DB) error {...})

// Call
resolver.Call(func(userRepo UserRepo) {...})
// Call 帶有返回值
// 這裏的 err 是依賴注入過程中的錯誤,比如依賴對象創建失敗
// results 是一個類型為 []interface{} 的數組,數組中按次序包含了 callback 函數的返回值,以下面的代碼為例,其中
// results[0] - string
// results[1] - error
results, err := resolver.Call(func(userRepo UserRepo) (string, error) {...})
// 由於每個返回值都是 interface{} 類型,因此在使用時需要執行類型斷言,將其轉換為具體的類型再使用
returnValue := results[0].(string)
returnErr := results[1].(error)

// AutoWire
// 假設我們有一個 UserRepo,創建該結構體時需要數據庫的連接實例
type UserRepo struct {
  db *sql.DB `autowire:"@"`
}

userRepo := UserRepo{}
resolver.AutoWire(&userRepo)

// 現在 userRepo 中的 db 參數已經自動被設置為了數據庫連接對象,可以繼續執行後續的操作了

Provider

在 Glacier 應用開發框架中,Provider 是應用模塊化的核心,每個獨立的功能模塊通過 Provider 完成實例初始化,每個 Provider 都需要實現 infra.Provider 接口。 在每個功能模塊中,我們通常會創建一個名為 provider.go 的文件,在該文件中創建一個 provider 實現

type Provider struct{}

func (Provider) Register(binder infra.Binder) {
    ... // 這裏可以使用 binder 向 IOC 容器註冊當前模塊中的實例創建方法
}

Provider 接口只有一個必須實現的方法 Register(binder infra.Binder),該方法用於註冊當前模塊的對象到 IOC 容器中,實現依賴注入的支持。

例如,我們實現一個基於數據庫的用户管理模塊 repo,該模塊包含兩個方法

package repo

type UserRepo struct {
  db *sql.DB
}

func (repo *UserRepo) Login(username, password string) (*User, error) {...}
func (repo *UserRepo) GetUser(username string) (*User, error) {...}

為了使該模塊能夠正常工作,我們需要在創建 UserRepo 時,提供 db 參數,在 Glacier 中,我們可以這樣實現

package repo

type Provider struct {}

func (Provider) Register(binder infra.Binder) {
  binder.Singleton(func(db *sql.DB) *UserRepo { return &UserRepo {db: db} })
}

在我們的應用創建時,使用 ins.Provider 方法註冊該模塊

ins := app.Default("1.0")
...
ins.MustSingleton(func() (*sql.DB, error) {
    return sql.Open("mysql", "user:pwd@tcp(ip:3306)/dbname")
})
// 在這裏加載模塊的 Provider
ins.Provider(repo.Provider{})
...
app.MustRun(ins)

ProviderBoot

在我們使用 Provider 時,默認只需要實現一個接口方法 Register(binder infra.Binder) 即可,該方法用於將模塊的實例創建方法註冊到 Glacier 框架的 IOC 容器中。

在 Glaicer 中,還提供了一個 ProviderBoot 接口,該接口包含一個 Boot(resolver Resolver) 方法,實現該方法的模塊,可以在 Glacier 框架啓動過程中執行一些模塊自有的業務邏輯,該方法在所有的模塊全部加載完畢後執行(所有的模塊的 Register 方法都已經執行完畢),因此,系統中所有的對象都是可用的。

Boot(resolver Resolver) 方法中適合執行一些在應用啓動過程中所必須完成的一次性任務,任務應該儘快完成,以避免影響應用的啓動。

type Provider struct{}

func (Provider) Register(binder infra.Binder) {
    binder.MustSingleton(func(conf *configs.Config) *grpc.Server { return ... })
}

func (Provider) Boot(resolver infra.Resolver) {
    resolver.MustResolve(func(serv *grpc.Server) {
        protocol.RegisterMessageServer(serv, NewEventService())
        protocol.RegisterHeartbeatServer(serv, NewHeartbeatService())
    })
}

DaemonProvider

模塊 Provider 的 Boot 方法是阻塞執行的,通常用於執行一些在應用啓動時需要執行的一些初始化任務,在一個應用中,所有的 Provider 的 Boot 方法是串行執行的。

DaemonProvider 接口則為模塊提供了異步執行的能力,模塊的 Daemon(ctx context.Context, resolver infra.Resolver) 方法是異步執行的,我們可以在這裏執行創建 web 服務器等操作。

func (Provider) Daemon(_ context.Context, app infra.Resolver) {
    app.MustResolve(func(
        serv *grpc.Server, conf *configs.Config, gf graceful.Graceful,
    ) {
        listener, err := net.Listen("tcp", conf.GRPCListen)
        ...
        gf.AddShutdownHandler(serv.GracefulStop)
        ...
        if err := serv.Serve(listener); err != nil {
            log.Errorf("GRPC Server has been stopped: %v", err)
        }
    })
}

ProviderAggregate

ProviderAggregate 接口為應用提供了一種能夠聚合其它模塊 Provider 的能力,在 Aggregate() []Provider方法中,我們可以定義多個我們當前模塊所依賴的其它模塊,在 Glacier 框架啓動過程中,會優先加載這裏定義的依賴模塊,然後再加載我們的當前模塊。

我們可以通過 ProviderAggregate 來創建我們自己的模塊, Aggregates() []infra.Provider 方法中返回依賴的子模塊,框架會先初始化子模塊,然後再初始化當前模塊。

// 創建自定義模塊,初始化了 Glacier 框架內置的 Web 框架
type Provider struct{}

func (Provider) Aggregates() []infra.Provider {
    return []infra.Provider{
        // 加載了 web 模塊,為應用提供 web 開發支持
        web.Provider(
            listener.FlagContext("listen"), // 從命令行參數 listen 獲取監聽端口
            web.SetRouteHandlerOption(s.routes), // 設置路由規則
            web.SetExceptionHandlerOption(func(ctx web.Context, err interface{}) web.Response {
                log.Errorf("error: %v, call stack: %s", err, debug.Stack())
                return nil
            }), // Web 異常處理
        ),
    }
}

func (Provider) routes(cc infra.Resolver, router web.Router, mw web.RequestMiddleware) {
    router.Controllers(
        "/api",
        // 這裏添加控制器
        controller.NewWelcomeController(cc),
        controller.NewUserController(cc),
    )
}

func (Provider) Register(app infra.Binder) {}

Service

在 Glacier 框架中,Service 代表了一個後台模塊,Service 會在框架生命週期中持續運行。要實現一個 Service,需要實現 infra.Service 接口,該接口只包含一個方法

  • Start() error 用於啓動 Service

除了 Start 方法之外,還支持以下控制方法,不過它們都是可選的

  • Init(resolver Resolver) error 用於 Service 的初始化,注入依賴等
  • Stop() 觸發 Service 的停止運行
  • Reload() 觸發 Service 的重新加載

以下為一個示例

type DemoService struct {
    resolver infra.Resolver
    stopped chan interface{}
}

// Init 可選方法,用於在 Service 啓動之前初始化一些參數
func (s *DemoService) Init(resolver infra.Resolver) error {
    s.resolver = resolver
    s.stopped = make(chan interface{})
    return nil
}

// Start 用於 Service 的啓動
func (s *DemoService) Start() error {
    for {
        select {
        case <-s.stopped:
            return nil
        default:
            ... // 業務代碼
        }
    }
}

// Stop 和 Reload 都是可選方法
func (s *DemoService) Stop() { s.stopped <- struct{}{} }
func (s *DemoService) Reload() { ... }

在我們的應用創建時,使用 app.Service 方法註冊 Service

ins := app.Create("1.0")
...
ins.Service(&service.DemoService{})
...
app.MustRun(ins)

ModuleLoadPolicy

ProviderService 支持按需加載,要使用此功能,只需要讓 ProviderService 實現 ShouldLoad(...) bool 方法。ShouldLoad 方法用於控制 ProviderService 是否加載,支持以下幾種形式

  • func (Provider) ShouldLoad(...依賴) bool
  • func (Provider) ShouldLoad(...依賴) (bool, error)

示例

type Provider struct{}
func (Provider) Register(binder infra.Binder) {...}

// 只有當 config.AuthType == ldap 的時候才會加載當前 Provider
func (Provider) ShouldLoad(config *config.Config) bool {
    return str.InIgnoreCase(config.AuthType, []string{"ldap"})
}

注意:ShouldLoad 方法在執行時,Provider 並沒有完成 Register 方法的執行,因此,在 ShouldLoad 方法的參數列表中,只能使用在應用創建時全局注入的對象實例。

ins := app.Create("1.0")
...
ins.Singleton(func(c infra.FlagContext) *config.Config { return ... })
...
app.MustRun(ins)

Priority

實現 infra.Priority 接口的 Provider Service ,會按照 Priority() 方法的返回值大小依次加載,值越大,加載順序越靠後,默認的優先級為 1000

type Provider struct {}
func (Provider) Register(binder infra.Binder) {...}

func (Provider) Priority() int {
    return 10
}

Web 框架

Glacier 是一個應用框架,為了方便 Web 開發,也內置了一個靈活的 Web 應用開發框架。

Usage

Glaicer Web 在 Glacier 框架中是一個內置的 DaemonProvider,與其它的模塊並無不同。我們通過 web.Provider(builder infra.ListenerBuilder, options ...Option) infra.DaemonProvider 方法創建 Web 模塊。

參數 builder 用於創建 Web 服務的 listener(用於告知 Web 框架如何監聽端口),在 Glaicer 中,有以下幾種方式來創建 listener:

  • listener.Default(listenAddr string) infra.ListenerBuilder 該構建器使用固定的 listenAddr 來創建 listener
  • listener.FlagContext(flagName string) infra.ListenerBuilder 該構建器根據命令行選項 flagName 來獲取要監聽的地址,以此來創建 listener
  • listener.Exist(listener net.Listener) infra.ListenerBuilder 該構建器使用應存在的 listener 來創建

參數 options 用於配置 web 服務的行為,包含以下幾種常用的配置

  • web.SetRouteHandlerOption(h RouteHandler) Option 設置路由註冊函數,在該函數中註冊 API 路由規則
  • web.SetExceptionHandlerOption(h ExceptionHandler) Option 設置請求異常處理器
  • web.SetIgnoreLastSlashOption(ignore bool) Option 設置路由規則忽略最後的 /,默認是不忽略的
  • web.SetMuxRouteHandlerOption(h MuxRouteHandler) Option 設置底層的 gorilla Mux 對象,用於對底層的 Gorilla 框架進行直接控制
  • web.SetHttpWriteTimeoutOption(t time.Duration) Option 設置 HTTP 寫超時時間
  • web.SetHttpReadTimeoutOption(t time.Duration) Option 設置 HTTP 讀超時時間
  • web.SetHttpIdleTimeoutOption(t time.Duration) Option 設置 HTTP 空閒超時時間
  • web.SetMultipartFormMaxMemoryOption(max int64) 設置表單解析能夠使用的最大內存
  • web.SetTempFileOption(tempDir, tempFilePattern string) Option 設置臨時文件存儲規則
  • web.SetInitHandlerOption(h InitHandler) Option 初始化階段,web 應用對象還沒有創建,在這裏可以更新 web 配置
  • web.SetListenerHandlerOption(h ListenerHandler) Option 服務初始化階段,web 服務對象已經創建,此時不能再更新 web 配置了

最簡單的使用 Web 模塊的方式是直接創建 Provider,

// Password 該結構體時 /complex 接口的返回值定義
type Password struct {
    Password string `json:"password"`
}

// Glacier 框架初始化
ins := app.Default("1.0")
...
// 添加命令行參數 listen,指定默認監聽端口 :8080
ins.AddStringFlag("listen", ":8080", "http listen address")
...
ins.Provider(web.Provider(
    // 使用命令行 flag 的 listener builder
    listener.FlagContext("listen"), 
    // 設置路由規則
    web.SetRouteHandlerOption(func(resolver infra.Resolver, r web.Router, mw web.RequestMiddleware) {
        ...
        r.Get("/simple", func(ctx web.Context, gen *password.Generator) web.Response {
            ...
            return ctx.JSON(web.M{"password": pass})
        })
        
        r.Get("/complex", func(ctx web.Context, gen *password.Generator) Password {...})
    }),
))

app.MustRun(ins)

更好的方式是使用模塊化,編寫一個獨立的 Provider

type Provider struct{}

// Aggregates 實現 infra.ProviderAggregate 接口
func (Provider) Aggregates() []infra.Provider {
    return []infra.Provider{
        web.Provider(
            confListenerBuilder{},
            web.SetRouteHandlerOption(routes),
            web.SetMuxRouteHandlerOption(muxRoutes),
            web.SetExceptionHandlerOption(exceptionHandler),
        ),
    }
}

// Register 實現 infra.Provider 接口
func (Provider) Register(binder infra.Binder) {}

// exceptionHandler 異常處理器
func exceptionHandler(ctx web.Context, err interface{}) web.Response {
    return ctx.JSONWithCode(web.M{"error": fmt.Sprintf("%v", err)}, http.StatusInternalServerError)
}

// routes 註冊路由規則
func routes(resolver infra.Resolver, router web.Router, mw web.RequestMiddleware) {
    mws := make([]web.HandlerDecorator, 0)
    // 添加 web 中間件
    mws = append(mws,
        mw.AccessLog(log.Module("api")),
        mw.CORS("*"),
    )

    // 註冊控制器,所有的控制器 API 都以 `/api` 作為接口前綴
    router.WithMiddleware(mws...).Controllers(
        "/api",
        controller.NewServerController(resolver),
        controller.NewClientController(resolver),
    )
}

func muxRoutes(resolver infra.Resolver, router *mux.Router) {
    resolver.MustResolve(func() {
        // 添加 prometheus metrics 支持
        router.PathPrefix("/metrics").Handler(promhttp.Handler())
        // 添加健康檢查接口支持
        router.PathPrefix("/health").Handler(HealthCheck{})
    })
}

// 創建自定義的 listener 構建器,從配置對象中讀取 listen 地址
type confListenerBuilder struct{}

func (l confListenerBuilder) Build(resolver infra.Resolver) (net.Listener, error) {
    return listener.Default(resolver.MustGet((*config.Server)(nil)).(*config.Server).HTTPListen).Build(resolver)
}

控制器

控制器必須實現 web.Controller 接口,該接口只有一個方法

  • Register(router Router) 用於註冊當前控制器的路由規則
type UserController struct {...}

// NewUserController 控制器創建方法,返回 web.Controller 接口
func NewUserController() web.Controller { return &UserController{...} }

// Register 註冊當前控制器關聯的路由規則
func (ctl UserController) Register(router web.Router) {
    router.Group("/users/", func(router web.Router) {
        router.Get("/", u.Users).Name("users:all")
        router.Post("/", u.Add)
        router.Post("/{id}/", u.Update)
        router.Get("/{id}/", u.User).Name("users:one")
        router.Delete("/{id}/", u.Delete).Name("users:delete")
    })

    router.Group("/users-helper/", func(router web.Router) {
        router.Get("/names/", u.UserNames)
    })
}

// 讀取 JSON 請求參數,直接返回實例,會以 json 的形式返回給客户端
func (ctl UserController) Add(ctx web.Context, userRepo repository.UserRepo) (*repository.User, error) {
    var userForm *UserForm
    if err := ctx.Unmarshal(&userForm); err != nil {
        return nil, web.WrapJSONError(fmt.Errorf("invalid request: %v", err), http.StatusUnprocessableEntity)
    }
    ctx.Validate(userForm, true)
    ...
    return ...
}

// 直接返回錯誤,如果 error 不為空,則返回錯誤給客户端
func (ctl UserController) Delete(ctx web.Context, userRepo repository.UserRepo) error {
    userID := ctx.PathVar("id")
    ...
    return userRepo.DeleteID(userID)
}

// 返回 web.Response,可以使用多種格式返回,如 ctx.Nil, ctx.API, ctx.JSON, ctx.JSONWithCode, ctx.JSONError, ctx.YAML, ctx.Raw, ctx.HTML, ctx.HTMLWithCode, ctx.Error 等
func (u UserController) Users(ctx web.Context, userRepo repository.UserRepo, roleRepo repository.RoleRepo) web.Response {
    page := ctx.IntInput("page", 1)
    perPage := ctx.IntInput("per_page", 10)
    ...
    return ctx.JSON(web.M{
        "users": users,
        "next":  next,
        "search": web.M{
            "name":  name,
            "phone": phone,
            "email": email,
        },
    })
}

使用 web.Router 實例的 Controllers 方法註冊控制器。

// routes 註冊路由規則
func routes(resolver infra.Resolver, router web.Router, mw web.RequestMiddleware) {
    mws := make([]web.HandlerDecorator, 0)
    // 添加 web 中間件
    mws = append(mws,
        mw.AccessLog(log.Module("api")),
        mw.CORS("*"),
    )

    // 註冊控制器,所有的控制器 API 都以 `/api` 作為接口前綴
    router.WithMiddleware(mws...).Controllers(
        "/api",
        controller.NewUserController(),
    )
}

事件管理

Glacier 框架提供了一個簡單的事件管理模塊,可以用於發佈和監聽應用運行中的事件,進行響應的業務處理。

通過 event.Provider(handler func(resolver infra.Resolver, listener Listener), options ...Option) infra.Provider 來初始化事件管理器。

ins.Provider(event.Provider(
  func(cc infra.Resolver, listener event.Listener) {
    listener.Listen(func(event CronEvent) {
      log.Debug("a new cron task executed")
      // 執行監聽到定時任務執行事件後要觸發的操作
    })
  },
  // 設置事件管理器選項
  event.SetStoreOption(func(cc infra.Resolver) event.Store {
    // 設置使用默認的內存事件存儲
    return event.NewMemoryEventStore(true, 100)
  }),
))

發佈事件時,使用 Glacier 框架的依賴注入能力,獲取 event.Publisher 接口實現

ins.Async(func(publisher event.Publisher) {
  for i := 0; i < 10; i++ {
    publisher.Publish(CronEvent{GoroutineID: uint64(i)})
  }
})

本地內存作為事件存儲後端

Glacier 內置了基於內存的事件存儲後端,説有事件的監聽器都是同步執行的。

// 設置事件管理器選項
event.SetStoreOption(func(cc infra.Resolver) event.Store {
    // 設置使用默認的內存事件存儲
    return event.NewMemoryEventStore(true, 100)
})

Redis 作為事件存儲後端

使用內存作為事件存儲後端時,當應用異常退出的時候,可能會存在事件的丟失,你可以使用這個基於 Redis 的事件存儲後端 redis-event-store 來獲得事件的持久化支持。

定時任務

Glacier 提供了內置的定時任務支持,使用 scheduler.Provider 來實現。

type Provider struct{}
func (Provider) Register(binder infra.Binder) {...}

func (Provider) Aggregates() []infra.Provider {
    return []infra.Provider{
        // 加載 scheduler 定時任務模塊
        scheduler.Provider(
            func(resolver infra.Resolver, creator scheduler.JobCreator) {
                // 添加一個名為 test-job 的任務,每隔 10s 執行一次
                _ = cr.Add("test-job", "@every 10s", TestJob)
                // 添加一個名稱為 test-timeout-job 的任務,每隔 5s 執行一次
                // 通過 AddAndRunOnServerReady 添加的任務會在服務啓動時先執行一次
                _ = creator.AddAndRunOnServerReady(
                    "test-timeout-job", 
                    "@every 5s",
                    // 使用 scheduler.WithoutOverlap 包裝的函數,當前一次調度還沒有執行完畢,本次調度的時間已到,本次調度將會被取消
                    scheduler.WithoutOverlap(TestTimeoutJob).SkipCallback(func() { 
                        ... // 當前一個任務還沒有執行完畢時,當前任務會被跳過,跳過時會觸發該函數的執行
                    }),
                )
            },
        ),
    }
}

scheduler.Provider 支持分佈式鎖,通過 SetLockManagerOption 選項可以指定分佈式鎖的實現,以滿足任務在一組服務器中只會被觸發一次的邏輯。

scheduler.Provider(
    func(resolver infra.Resolver, creator scheduler.JobCreator) {...},
    // 設置分佈式鎖
    scheduler.SetLockManagerOption(func(resolver infra.Resolver) scheduler.LockManagerBuilder {
        // get redis instance
        redisClient := resolver.MustGet(&redis.Client{}).(*redis.Client)
        return func(name string) scheduler.LockManager {
            // create redis lock
            return redisLock.New(redisClient, name, 10*time.Minute)
        }
    }),
)
注意:Glacier 框架沒有內置分佈式鎖的實現,在 mylxsw/distribute-locks 實現了一個簡單的基於 Redis 的分佈式鎖實現,可以參考使用。

日誌

在 Glacier 中,默認使用 asteria 作為日誌框架,asteria 是一款功能強大、靈活的結構化日誌框架,支持多種日誌輸出格式以及輸出方式,支持為日誌信息添加上下文信息。

最簡單的方式是通過 log.SetDefaultLogger(logger infra.Logger) 方法為 Glacier 框架設置默認的日誌處理器,

// import "github.com/mylxsw/glacier/log"

// 默認設置,使用 asteria 日誌框架
// import asteria "github.com/mylxsw/asteria/log"
log.SetDefaultLogger(asteria.Module("glacier"))
// 使用標準庫中的日誌包,Glacier 對標準庫日誌包進行了簡單封裝
log.SetDefaultLogger(log.StdLogger())

當然,如果使用了 starter 模板項目創建的應用,也可以使用 WithLogger(logger infra.Logger) 方法來設置日誌處理器。

ins := app.Default("1.0")
...
// 設置使用標準庫日誌包,不輸出 DEBUG 日誌
ins.WithLogger(log.StdLogger(log.DEBUG))
...

除了默認的 asteria 日誌庫以及 Glacier 自帶的 StdLogger 之外,還可以使用其它第三方的日誌包,只需要簡單的封裝,實現 infra.Logger 接口即可。

type Logger interface {
    Debug(v ...interface{})
    Debugf(format string, v ...interface{})
    Info(v ...interface{})
    Infof(format string, v ...interface{})
    Error(v ...interface{})
    Errorf(format string, v ...interface{})
    Warning(v ...interface{})
    Warningf(format string, v ...interface{})
    // Critical 關鍵性錯誤,遇到該日誌輸出時,應用直接退出
    Critical(v ...interface{})
    // Criticalf 關鍵性錯誤,遇到該日誌輸出時,應用直接退出
    Criticalf(format string, v ...interface{})
}

Eloquent ORM

Eloquent ORM 是為 Go 開發的一款數據庫 ORM 框架,它的設計靈感來源於著名的 PHP 開發框架 Laravel,支持 MySQL 等數據庫。

項目地址為 mylxsw/eloquent,可以配合 Glacier 框架使用。

平滑退出

Glacier 支持平滑退出,當我們按下鍵盤的 Ctrl+C 時(接收到 SIGINT, SIGTERM, Interrupt 等信號), Glacier 將會接收到關閉的信號,然後觸發應用的關閉行為。默認情況下,我們的應用會立即退出,我們可以通過 starter 模板創建的應用上啓用平滑支持選項 WithShutdownTimeoutFlagSupport(timeout time.Duration) 來設置默認的平滑退出時間

ins := app.Create("1.0")
ins.WithShutdownTimeoutFlagSupport(5 * time.Second)
...

// Provider 中獲取 `gf.Graceful` 實例,註冊關閉時的處理函數
resolver.MustResolve(func(gf graceful.Graceful) {
    gf.AddShutdownHandler(func() {
        ...
    })
})

第三方框架集成

  • giris: Iris Web Framework 適配

示例項目

  • Example 使用示例
  • WebDAV Server 一款支持 LDAP 作為用户數據庫的 WebDAV 服務器
  • Adanos Alert 一個功能強大的開源告警平台,通過事件聚合機制,為監控系統提供釘釘、郵件、HTTP、JIRA、語音電話等告警方式的支持
  • Healthcheck 為應用服務提供健康檢查告警支持
  • Sync 跨服務器文件同步服務
  • Tech Share 一個用於中小型團隊內部技術分享管理的 Web 應用
  • Universal Exporter 一個通用的 Prometheus 維度工具,目前支持從數據庫中查詢生成 Metric 數據
  • Graphviz Server 一個 Web 服務,封裝了對 Graphviz 的接口調用,實現通過 Web API 的方式生成 Graphviz 圖形
  • MySQL Guard 用於 MySQL 長事務檢測殺死和死鎖告警
  • Password Server 一個生成隨機密碼的簡單 web 服務器
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.