第 12 章
[Day 12] Dependency Injection
Dependency Injection is a design pattern for initializing structs.
Let’s review NewApp() in cmd/olivine/app.go
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
}
I need server.Server{} to store in App{}, and it requires server.Handler{}, server.Restorer{}. It calls App{} depends on server.Server{} and server.Handler{}; and server.Handler{} depends on service.AOF{}, …
Dependency Inection Library
Depencency injection can be managed by self, what I do in NewApp() implementation, but it’s easier when using a library.
There’re 2 different methods to implement dependency injection:
- Compile Time:
go generate - Runtime:
reflect
I prefer compile time dependency injection, because it generates a readable / analyzable go source code everything expected.
Although runtime dependency injection is good, but there’re too many “magic” to debug because of reflect.
Kessoku: a wire alternative
google/wire is a good compile time dependency injection library, but it is archived on Aug, 2025. I used mazrean/kessoku as alternative.
Install it is very easy:
$ go get -tool github.com/mazrean/kessoku/cmd/kessoku
Note: go module supports tool management on Go 1.24+, it’s no need to install tools globally.
Migrate NewApp() to kessoku
Create a file 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 }),
)
The kessoku.Inject hints kessoku to generate a constructor NewApp() by given cfg *data.Config and returns (*App, error)
We can move NewApp() here directly:
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
}),
)
Don’t forget to remove NewApp() from app.go.
$ go generate ./...
Initialize dependencies
It still initialize server.Server{}, server.Handler{}, server.Restorer{} in single kessoku provider.
It can be moved to other kessoku provider easily:
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,
}
}),
)
Tips for Dependency Injection
Sometimes, I could need an interface Server{}:
interface Server {
Handle()
Restore()
}
However, I defined Handle() in service.Handler{}, and Restore() in service.Restorer{}. I can package *service.Handler{} and *service.Restorer{} in a anonymous struct, and inject by 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 }
}),
),
Refactor goroutine with errgroup
It uses error channel with goroutine at this moment, we can refactor it with 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() }()