不知道大家在用 Go 語言 寫服務的時候,會不會遇到 Components 會有相互依賴的關係,A 物件依賴 B 物件,B 物件又依賴 C 物件,所以在初始化 A 物件前,就必須先將 B 跟 C 初始化完成,這就是錯綜復雜的關係。也許大家會想到另一個做法,就是把每個物件都宣告成全域變數 ,我個人不推薦這個使用方式,雖然很方便,但是就會讓整體架構變得很複雜。而本篇要介紹一個救星工具,就是 Google 團隊開發的 Wire 工具,官方部落格也可以參考看看 。此工具就是為了解決底下兩個問題 (dependency injection )。
Components 互相依賴錯綜復雜的關係 不要宣告全域變數 教學影片 VIDEO
1
2
3
4
5
6
7
8
9
10
11
00:00 Dependency Injection 是什麼
00:23 模組相依性產生的問題
02:09 用程式碼講解問題出在哪邊
05:29 撰寫 wire.go 代碼,宣告 application struct
06:52 撰寫 inject_router.go
07:49 撰寫 inject_user.go
09:06 產生 wire_gen.go 代碼
10:36 安裝 wire 工具
11:17 wire_gen.go 內容是什麼
12:23 如何簡化 main.go 內容
14:04 如何再次修改 dependency
其他線上課程請參考如下
模組依賴問題 底下所有程式碼都可以在這邊找到 。
大家參考上面這張圖,開發者想要在 main.go
內宣告 User 的 struct,就會需要一層一層依賴,所以代碼會寫成如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
cfg, err := config.Environ ()
if err != nil {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
c, err := cache.New (cfg)
if err != nil {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
l, err := ldap.New (cfg, c)
if err != nil {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
cd, err := crowd.New (cfg, c)
if err != nil {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
u, err := user.New (l, cd, c)
if err != nil {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
if ok := u.Login ("test" , "test" ); !ok {
log.Fatal ().
Err (err).
Msg ("invalid configuration" )
}
m := graceful.NewManager ()
srv := &http.Server{
Addr: cfg.Server.Port,
Handler: router.New (cfg, u),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
MaxHeaderBytes: 8 * 1024 , // 8KiB
}
上述代碼大家可以想一下,如果是幾 10 個 Components 寫起來就會更加複雜。其實我們在主程式內只有用到 user
跟 router
而已,光是宣告其他 component 就寫了一堆代碼,我們是否可以將代碼優化成一個 struct
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type application struct {
router http.Handler
user *user.Service
}
func newApplication (
router http.Handler,
user *user.Service,
) *application {
return &application{
router: router,
user: user,
}
}
這樣只要宣告 application 中間的 dependency 都透過 wire 幫忙處理。
使用 Wire 工具 1
2
3
4
5
6
7
8
9
func newApplication (
router http.Handler,
user *user.Service,
) *application {
return &application{
router: router,
user: user,
}
}
要讓 wire 工具可以知道上面全部的依賴關係,先建立一個初始化函式,注意 wireinject
這個 build 標籤是不能拿掉的,這是給 wire CLI 工具辨認用的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//go:build wireinject
// +build wireinject
package main
import (
"github.com/go-training/example49-dependency-injection/config"
"github.com/google/wire"
)
func InitializeApplication (cfg config.Config) (*application, error ) {
wire.Build (
routerSet,
userSet,
newApplication,
)
return &application{}, nil
}
大家可以看到我們需要告知 wire 工具,router, user 及 newApplication 的關係,所以在分別建立 inject_router.go
跟 inject_user.go
兩個檔案,先看看 router 部分
1
2
3
4
5
6
7
8
9
10
var routerSet = wire.NewSet ( //nolint:deadcode,unused,varcheck
provideRouter,
)
func provideRouter (
cfg config.Config,
user *user.Service,
) http.Handler {
return router.New (cfg, user)
}
這邊只有依賴 config 跟 user 兩物件並回傳 http.Handler,接下來 user 部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var userSet = wire.NewSet ( //nolint:deadcode,unused,varcheck
provideUser,
provideLDAP,
provideCROWD,
provideCache,
)
func provideUser (
l *ldap.Service,
c *crowd.Service,
cache *cache.Service,
) (*user.Service, error ) {
return user.New (l, c, cache)
}
func provideLDAP (
cfg config.Config,
cache *cache.Service,
) (*ldap.Service, error ) {
return ldap.New (cfg, cache)
}
func provideCROWD (
cfg config.Config,
cache *cache.Service,
) (*crowd.Service, error ) {
return crowd.New (cfg, cache)
}
func provideCache (
cfg config.Config,
) (*cache.Service, error ) {
return cache.New (cfg)
}
user 依賴了 cache, ldap 跟 crowd 三個物件,完成後就可以透過 wire 指令產生 wire_gen.go
檔案
打開 wire_gen.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func InitializeApplication (cfg config.Config) (*application, error ) {
service, err := provideCache (cfg)
if err != nil {
return nil , err
}
ldapService, err := provideLDAP (cfg, service)
if err != nil {
return nil , err
}
crowdService, err := provideCROWD (cfg, service)
if err != nil {
return nil , err
}
userService, err := provideUser (ldapService, crowdService, service)
if err != nil {
return nil , err
}
handler := provideRouter (cfg, userService)
mainApplication := newApplication (handler, userService)
return mainApplication, nil
}
可以看到透過我們自己定義的 provide
前綴的函式,wire 工具自動幫我們把相依信都處理完畢,所以這工具讓你在主程式內把所以相依性都處理完畢,不要再使用全域變數 了。
心得 除了使用在 main 函式,還可以用在測試上面,測試也是要把依賴性都處理完畢,這樣才方便測試。相信大家處理依賴性肯定會遇到這問題。好的做法就是不要在 package 內宣告其他 package 的設定,這樣會非常難維護,畢竟一開始把所有的物件都初始化完畢,除錯的時候會相對容易。程式碼可以這邊觀看 。
See also