Olivine: Building a Redis-Compatible Service in Go. · 章節目錄

第 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:

  1. Compile Time: go generate
  2. 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() }()