第 12 章
Day 12:Dependency Injection
Dependency Injection 是管理 struct 初始化與依賴的設計模式。
回顧 cmd/olivine/app.go 的 NewApp():
func NewApp(cfg *data.Config) (*App, error) {
var handler server.Handler
var restorer server.Restorer
var aof service.AOF
if cfg.AOFEnabled {
var err error
aof, err = service.NewAOF(cfg, AOFPath)
if err != nil {
return nil, err
}
handler = server.NewHandler(cmd.NewCommands(repo.NewStorage()), server.NewAOFMiddleware(aof))
restorer = server.NewRestorer(aof, handler)
} else {
handler = server.NewHandler(cmd.NewCommands(repo.NewStorage()))
}
return &App{
cfg: cfg,
aof: aof,
srv: server.NewServer(handler, restorer),
}, nil
}
App 需要 server.Server,而 server 又需要 server.Handler 與 server.Restorer;handler 也會依賴 service.AOF 等元件,形成依賴關係。
Dependency Injection Library
可以像目前的 NewApp() 一樣自行管理依賴,也可以交給 library。
常見方式有兩種:
- Compile Time:透過
go generate產生程式碼。 - Runtime:透過
reflect建立依賴。
我偏好 compile time,因為產生的 Go 原始碼可閱讀、可分析。Runtime 方案也有優點,但 reflect 帶來的隱藏行為比較難除錯。
Kessoku:wire 的替代方案
google/wire 已於 2025 年八月封存,因此我使用 mazrean/kessoku 作為替代。
安裝方式:
$ go get -tool github.com/mazrean/kessoku/cmd/kessoku
注意:Go 1.24 起支援 module 工具管理,不必將工具安裝到全域。
將 NewApp() 移到 kessoku
建立 cmd/olivine/kessoku.go:
// file: cmd/olivine/kessoku.go
package main
import (
"github.com/mazrean/kessoku"
)
//go:generate go tool kessoku $GOFILE
var _ = kessoku.Inject[*App](
"NewApp",
kessoku.Provide(func(cfg *data.Config) (*App, error) { return nil, nil }),
)
kessoku.Inject 指示產生名為 NewApp() 的 constructor,接收 cfg *data.Config 並回傳 (*App, error)。
可以先將原本 NewApp() 的內容直接搬入:
var _ = kessoku.Inject[*App](
"NewApp",
kessoku.Provide(func(cfg *data.Config) (*App, error) {
var handler server.Handler
var restorer server.Restorer
var aof service.AOF
if cfg.AOFEnabled {
var err error
aof, err = service.NewAOF(cfg, AOFPath)
if err != nil {
return nil, err
}
handler = server.NewHandler(cmd.NewCommands(repo.NewStorage()), server.NewAOFMiddleware(aof))
restorer = server.NewRestorer(aof, handler)
} else {
handler = server.NewHandler(cmd.NewCommands(repo.NewStorage()))
}
return &App{
cfg: cfg,
aof: aof,
srv: server.NewServer(handler, restorer),
}, nil
}),
)
別忘了從 app.go 移除 NewApp()。
$ go generate ./...
初始化依賴
目前仍在同一個 provider 裡初始化 server、handler、restorer,可以拆成個別 provider:
var _ = kessoku.Inject[*App](
"NewApp",
kessoku.Bind[service.AOF](kessoku.Provide(func(cfg *data.Config) (service.AOF, error) {
if !cfg.AOFEnabled {
return nil, nil
}
return service.NewAOF(cfg, AOFPath)
})),
kessoku.Bind[server.Handler](kessoku.Provide(func(cfg *data.Config, aof service.AOF) server.Handler {
middlewares := []server.Middleware{}
if cfg.AOFEnabled {
middlewares = append(middlewares, server.NewAOFMiddleware(aof))
}
return server.NewHandler(cmd.NewCommands(repo.NewStorage()), middlewares...)
})),
kessoku.Bind[server.Restorer](kessoku.Provide(func(cfg *data.Config, aof service.AOF, handler server.Handler) server.Restorer {
if !cfg.AOFEnabled {
return nil
}
return server.NewRestorer(aof, handler)
})),
kessoku.Bind[server.Server](kessoku.Provide(server.NewServer)),
kessoku.Provide(func(cfg *data.Config, aof service.AOF, srv server.Server) *App {
return &App{
cfg: cfg,
aof: aof,
srv: srv,
}
}),
)
Dependency Injection 技巧
假設需要以下 Server interface:
interface Server {
Handle()
Restore()
}
而 Handle() 和 Restore() 分別定義在 service.Handler、service.Restorer,就可以將它們嵌入匿名 struct,再交給 kessoku 注入:
kessoku.Provide(service.NewHandler), // create `*service.Handler{}`
kessoku.Provide(service.NewRestorer), // create `*service.Restorer{}`
kessoku.Bind[server.Server](
kessoku.Provide(func(h *service.Handler, r *service.Restorer) server.Server {
return &struct{
*service.Handler,
*service.Restorer,
}{ Handler: h, Restorer: r }
}),
),
使用 errgroup 重構 goroutine
目前以 error channel 管理 goroutines,可以改用 errgroup:
- errch := make(chan error, 1)
- if app.cfg.AOFEnabled && app.cfg.AOFFsync == data.AOFFsyncEverySec {
- go func() {
- ticker := time.NewTicker(time.Second)
- defer ticker.Stop()
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- if err := app.aof.Sync(); err != nil {
- errch <- err
- return
- }
- }
- }
- }()
- }
- go func() {
- slog.Info("starting olivine server")
- if err := app.srv.ListenAndServe(); err != nil && !errors.Is(err, server.ErrServerClosed) {
- errch <- err
- return
- }
- errch <- nil
- }()
+ g, ctx := errgroup.WithContext(ctx)
+ if app.cfg.AOFEnabled && app.cfg.AOFFsync == data.AOFFsyncEverySec {
+ g.Go(func() error {
+ ticker := time.NewTicker(time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return nil
+ case <-ticker.C:
+ if err := app.aof.Sync(); err != nil {
+ return err
+ }
+ }
+ }
+ })
+ }
+
+ g.Go(func() error {
+ slog.Info("starting olivine server")
+ if err := app.srv.ListenAndServe(); err != nil && !errors.Is(err, server.ErrServerClosed) {
+ return err
+ }
+ return nil
+ })
+
+ errch := make(chan error, 1)
+ go func() { errch <- g.Wait() }()