iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0

Dependency Injectino(依賴注入) 為了簡化初始化結構的設計模式。

在開始之前,我們先來檢視一下目前的 main.NewApp()

// file: ./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
}

不難發現,結構的建立往往是環環相扣的,父結構依賴於子結構:

  • 建立 main.(*App),我們需要 server.(Server)
    • 建立 server.(Server),所以需要 server.(Handler)server.(Handler)
      • 建立 server.(Handler),所以需要 server.(AOF)

此時,就是使用依賴注入的好時機。

依賴注入函式庫

依賴注入可以像目前一樣手動管理(就像現在 main.NewApp() 中的做法一樣),而使用函式庫進行管理往往能夠簡化我們的心智負擔。

主流有兩種依賴注入實現方法:

  • 編譯期注入:利用 go generate 自動生成依賴注入
  • 執行期注入:利用 reflect 特性自動完成依賴注入

我個人偏好使用編譯期注入,因為它可以生成可讀、可分析的 go 原始碼,所有事情都是可預期的;雖然執行期注入的方式可以讓開發者少寫很多 code,但 reflect 的使用上讓它有著太多的「魔法」。

Kessoku:wire 的替代品

google/wire 是一個很優秀的編譯期依賴注入函式庫,但是它在 2025 年 8 月就被標記為 archived。我個人使用 mazrean/kessoku 作為替代。

安裝它非常簡單:

$ go get -tool github.com/mazrean/kessoku/cmd/kessoku

註:go module 在 Go 1.24 之後支援工具管理,可以不用 go install 在全域安裝工具。

遷移 main.NewApp()

建立 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 會提示 kessoku 以 go generate 以給定的 cfg *data.Config 來產生 NewApp()

我們可以直接將 main.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
	}),
)

刪除原本的 main.NewApp() 之後便可以用 go generate ./... 產生 cmd/olivine/kessuku_band.go

相依初始化

目前,我們仍是手動管理整個套件的相依性而未能享受到 kessoku 所帶來的便利。

利用 kessoku.Bind()kessoku.Privde() 來改寫:

// file: ./cmd/olivine/kessoku.go
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,
		}
	}),
)

一些依賴注入的小技巧

有的時候,我們可能會定義一個介面:

interface Server {
	Handle()
	Restore()
}

但是卻在不同的 service 中定義了分別實現了這個介面:例如,在 service.(Handler) 中定義了 Handle()、而在 service.(Restorer) 定義了 Restore()

這時,我們可以用一個匿名介面來解決這個問題:

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 }
	}),
),

上一篇
[Day 21] 再論 AOF
下一篇
[Day 23] 過期處理(上):資料結構與被動刪除
系列文
從零開始打造 Redis:以 Go 建立 Production Ready 應用程式 29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言