Go 工程化 -三- 依賴注入框架 wire
序
在上一篇文章當中我們講到了項目的目錄結構,大體上水平切分爲了四層,然後再根據需要進行垂直切分,然後由於我們大量的使用到了接口和依賴注入的手段,所以在項目初始化的時候如果手動進行依賴關係的初始化會比較麻煩,這時候就需要用到依賴注入的框架了。
在剛開始接觸 go 的那一段時間,我是比較排斥使用太多框架的,覺得保持簡單更加重要,這種想法在很長一段時間(大概兩年左右)都沒有問題,直到正式工作一段時間之後發現,隨着開發合作的同學的增多以及部門的要求增加,項目啓動時的依賴越來越多,依賴之間還有先後順序,有一些甚至是隱式的順序,到時 main 函數的代碼膨脹的非常迅速並且慢慢的變的不可維護了,這種情況下引入依賴注入框架其實可以省心很多。
Golang 的依賴注入框架有兩類,一類是通過反射在運行時進行依賴注入,典型代表是 uber 開源的 dig,另外一類是通過 generate 進行代碼生成,典型代表是 Google 開源的 wire。使用 dig 功能會強大一些,但是缺點就是錯誤只能在運行時才能發現,這樣如果不小心的話可能會導致一些隱藏的 bug 出現。使用 wire 的缺點就是功能限制多一些,但是好處就是編譯的時候就可以發現問題,並且生成的代碼其實和我們自己手寫相關代碼差不太多,更符合直覺,心智負擔更小,所以我更加推薦 wire,如果對 dig 感興趣可以跳轉到文章參考文獻處跳轉查閱。
本文分爲兩個部分,首先介紹 wire 的使用方法,然後是結合上一篇文章中的工程目錄,我在使用 wire 過程中的一些 “最佳實踐” 避免大家重複踩坑。
wire 使用教程
如果你對 wire 已經比較熟悉可以直接跳過這一部分,閱讀完本文之後建議對照看一下官方文檔再進行操作。注:本文基於 wire 0.5.0 進行編寫
安裝
安裝很簡單,只要安裝了 Go 並且已經把 $GOPATH/bin
加入到了 PATH
當中,終端執行下面的語句即可
go get github.com/google/wire/cmd/wire
Provider
正式開始前需要先了解一下 wire 當中的兩個概念:provider 和 injector
Provider 是一個普通的函數,這個函數會返回構建依賴關係所需的組件。如下所示,就是一個 provider 函數,在實際使用的時候,往往是一些簡單的工廠函數,這個函數不會太複雜。
// NewPostRepo 創建文章 Repo
func NewPostRepo() IPostRepo {}
不過需要注意的是**「在 wire 中不能存在兩個 provider 返回相同的組件類型」**
Injector
injector 也是一個普通函數,我們常常在 wire.go
文件中定義 injector 函數簽名,然後通過 wire
命令自動生成一個完整的函數
//+build wireinject
func GetBlogService() *Blog {
panic(wire.Build(NewBlogService, NewPostUsecase, NewPostRepo))
}
第一行的 //+build wireinject
註釋確保了這個文件在我們正常編譯的時候不會被引用,而 wire .
生成的文件 wire_gen.go
會包含 //+build !wireinject
註釋,正常編譯的時候,不指定 tag 的情況下會引用這個文件
wire.Build
在 injector
函數中使用,用於表名這個 injector
由哪些 provider
提供依賴, injector
函數本身只是一個函數簽名,所以我們直接在函數中 panic
實際生成代碼的時候並不會直接調用 panic
一個完整的 🌰
基本示例
package example
// repo
// IPostRepo IPostRepo
type IPostRepo interface{}
// NewPostRepo NewPostRepo
func NewPostRepo() IPostRepo {
return new(IPostRepo)
}
// usecase
// IPostUsecase IPostUsecase
type IPostUsecase interface{}
type postUsecase struct {
repo IPostRepo
}
// NewPostUsecase NewPostUsecase
func NewPostUsecase(repo IPostRepo) IPostUsecase {
return postUsecase{repo: repo}
}
// service service
// PostService PostService
type PostService struct {
usecase IPostUsecase
}
// NewPostService NewPostService
func NewPostService(u IPostUsecase) *PostService {
return &PostService{usecase: u}
}
上面的是一個簡單的示例, NewPostService
NewPostUsecase
這些都是 Provider
函數,下面我們在 wire.go
當中構建 Injector
函數簽名
//+build wireinject
package example
import "github.com/google/wire"
func GetPostService() *PostService {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}
我們在目錄下執行 wire .
生成如下文件,可以看到生成的函數和我們自己手寫其實差不多
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package example
// Injectors from wire.go:
func GetPostService() *PostService {
iPostRepo := NewPostRepo()
iPostUsecase := NewPostUsecase(iPostRepo)
postService := NewPostService(iPostUsecase)
return postService
}
缺少 provider
在執行 wire .
的時候,如果我們的缺少某個 Provider
提供依賴,wire 會進行提示,幫助我們快速找到問題並且修改 還是上面的這個例子,我們刪除掉一個 Provider 函數試試
func GetPostService() *PostService {
panic(wire.Build(
NewPostService,
NewPostUsecase,
))
}
再次執行 wire
命令,可以發現報錯
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/wire.go:7:1: inject GetPostService: no provider found for github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostRepo
needed by github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostUsecase in provider "NewPostUsecase" (/Go-000/Week04/blog/03_wire/01_example/example.go:22:6)
needed by *github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.PostService in provider "NewPostService" (/Go-000/Week04/blog/03_wire/01_example/example.go:34:6)
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure
返回錯誤
在 go 中如果遇到錯誤,我們會在最後一個返回值返回 error,wire 同樣也支持返回錯誤的情況,只需要在 injector 的函數簽名中加上 error 返回值即可,還是前面的那個例子,我們讓 NewPostService
返回 error,並且修改 GetPostService
這個 Injector
函數
// example.go
// NewPostService NewPostService
func NewPostService(u IPostUsecase) (*PostService, error) {
return &PostService{usecase: u}, nil
}
// wire.go
func GetPostService() (*PostService, error) {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}
生成的代碼如下所示,可以發現會像我們自己寫代碼一樣判斷一下 if err
然後返回
// wire_gen.go
func GetPostService() (*PostService, error) {
iPostRepo := NewPostRepo()
iPostUsecase := NewPostUsecase(iPostRepo)
postService, err := NewPostService(iPostUsecase)
if err != nil {
return nil, err
}
return postService, nil
}
清理函數
有時候我們需要打開文件,或者是鏈接這種需要關閉的資源,這時候 provider 可以返回一個閉包函數 func()
,wire 在進行構建的時候,會在報錯的時候調用,並且會將所有的閉包函數聚合返回。這個特性一般用的不多,但是有需求的時候會十分有用。
還是之前的示例,我們修改一下 NewPostRepo
NewPostUsecase
讓他們返回一個清理函數
// example.go
// NewPostRepo NewPostRepo
func NewPostRepo() (IPostRepo, func(), error) {
return new(IPostRepo), nil, nil
}
// NewPostUsecase NewPostUsecase
func NewPostUsecase(repo IPostRepo) (IPostUsecase, func(), error) {
return postUsecase{repo: repo}, nil, nil
}
// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
NewPostService,
NewPostUsecase,
NewPostRepo,
))
}
執行 wire .
之後我們可以發現生成的函數當中,當 NewPostUsecase
出現錯誤的時候會自動幫我們調用 NewPostRepo
返回的 cleanup
函數,而 NewPostService
返回錯誤,會調用它依賴的所有 provider 的 cleanup 函數,如果都沒有問題,就會把所有 cleanup 函數聚合爲一個函數返回
func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
iPostUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
postService, err := NewPostService(iPostUsecase)
if err != nil {
cleanup2()
cleanup()
return nil, nil, err
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}
高級方法
接口注入
我們應該依賴接口,而不是實現。返回數據的時候返回實現而不是接口,這是在 Golang 中的最佳實踐(當然也不是所有的都是這樣),所以如果我們的 provider 返回了實現,但是我們的依賴的是接口,這時候就會報錯了,我們先來看一個例子。
我們修改一下 NewPostUsecase
方法,讓他返回 *PostUsecase
而不是接口
// NewPostUsecase NewPostUsecase
func NewPostUsecase(repo IPostRepo) (*PostUsecase, func(), error) {
return &PostUsecase{repo: repo}, nil, nil
}
這時候執行 wire .
生成代碼會發現報錯,找不到 IPostUsecase
的 provider
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/wire.go:7:1: inject GetPostService: no provider found for github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.IPostUsecase
needed by *github.com/mohuishou/go-training/Week04/blog/03_wire/01_example.PostService in provider "NewPostService" (/Go-000/Week04/blog/03_wire/01_example/example.go:36:6)
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure
這時候就需要使用 wire.Bind
將 Struct
和接口進行綁定了,表示這個結構體實現了這個接口,我們修改一下 injector
函數
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
NewPostService,
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
))
}
wire.Bind
的使用方法就是 wire.Bind(new(接口), new(實現))
Struct 屬性注入
在上面 NewPostService
代碼,我們可以發現有很多 Struct
的初始化其實就是填充裏面的屬性,沒有其他的邏輯,這種情況我們可以偷點懶直接使用 wire.Struct
方法直接生成 provider
// structType: 結構體類型
// fieldNames: 需要填充的字段,使用 "*" 表示所有字段都需要填充
Struct(structType interface{}, fieldNames ...string)
我們修改一下 Injector
函數
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
// 這裏由於只有一個字段,所以這兩種是等價的 wire.Struct(new(PostService), "*"),
wire.Struct(new(PostService), "usecase"),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
))
}
可以看到生成的代碼當中自動就生成了一個結構體並且填充數據了
func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
postUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
// 注意這裏
postService := &PostService{
usecase: postUsecase,
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}
值綁定
除了依賴某一個類型之外,有時候我們還會依賴一些具體的值,這時候我們就可以使用 wire.Value
或者是 wire.InterfaceValue
,爲某個類型綁定具體的值
// wire.Value 爲某個類型綁定值,但是不能爲接口綁定值
Value(interface{}) ProvidedValue
// wire.InterfaceValue 爲接口綁定值
InterfaceValue(typ interface{}, x interface{}) ProvidedValue
我們修改一下 PostService
使他依賴一個 int 和 io.Reader 然後爲它直接綁定 a=99
io.Reader = os.Stdin
// example.go
type PostService struct {
usecase IPostUsecase
a int
r io.Reader
}
// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
wire.Struct(new(PostService), "*"),
wire.Value(10),
wire.InterfaceValue(new(io.Reader), os.Stdin),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
))
}
可以看到生成的代碼當中直接生成了兩個全局變量
func GetPostService() (*PostService, func(), error) {
iPostRepo, cleanup, err := NewPostRepo()
if err != nil {
return nil, nil, err
}
postUsecase, cleanup2, err := NewPostUsecase(iPostRepo)
if err != nil {
cleanup()
return nil, nil, err
}
int2 := _wireIntValue
reader := _wireFileValue
postService := &PostService{
usecase: postUsecase,
a: int2,
r: reader,
}
return postService, func() {
cleanup2()
cleanup()
}, nil
}
// 注意這裏
var (
_wireIntValue = 10
_wireFileValue = os.Stdin
)
ProviderSet(Provider 集合)
在真實的項目當中依賴往往是一組一組的,就像我們的示例一樣,只要依賴 PostService
那麼 NewPostUsecase
NewPostRepo
這兩個就必不可少,所以我們往往會創建一些 ProviderSet
在 Injector
函數中直接依賴 ProviderSet
就可以了
// 參數是一些 provider
NewSet(...interface{}) ProviderSet
示例如下所示,生成代碼和之前一樣就不另外貼了
// example.go
// PostServiceSet PostServiceSet
var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(10),
wire.InterfaceValue(new(io.Reader), os.Stdin),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
)
// wire.go
func GetPostService() (*PostService, func(), error) {
panic(wire.Build(
PostServiceSet,
))
}
wire 使用最佳實踐
不要使用默認類型
之前有提到過,wire 不支持兩個提供兩個相同類型的 provider,所以如果我們使用默認類型如 int
string
等,只要有兩個依賴就會導致報錯,解決方案是使用類型別名。先來看一個報錯的示例
type PostService struct {
usecase IPostUsecase
a int
b int
r io.Reader
}
可以看到,wire 在構建依賴關係的時候,並不知道 int 的值該分配給 a 還是 b 所以就會報錯
▶ wire .
wire: /Go-000/Week04/blog/03_wire/01_example/example.go:40:2: provider struct has multiple fields of type int
wire: github.com/mohuishou/go-training/Week04/blog/03_wire/01_example: generate failed
wire: at least one generate failure
我們自定義兩個類型就好了
type A int
type B int
// PostService PostService
type PostService struct {
usecase IPostUsecase
a A
b B
r io.Reader
}
// PostServiceSet PostServiceSet
var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(A(10)),
wire.Value(B(10)),
wire.InterfaceValue(new(io.Reader), os.Stdin),
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
NewPostUsecase,
NewPostRepo,
)
這種方式在使用上會感覺有點糟心,但是就我目前的使用來看,用到基礎類型的情況還是比價少,所以也還好
Option Struct
在實際的業務場景當中我們的 NewXXX
函數的參數列表可能會很長,這個時候就可以直接定義一個 Option Struct 然後使用 wire.Strcut
來構建 Option Strcut 的依賴
// PostUsecaseOption PostUsecaseOption
type PostUsecaseOption struct {
a A
b B
repo IPostRepo
}
// NewPostUsecase NewPostUsecase
func NewPostUsecase(opt *PostUsecaseOption) (*PostUsecase, func(), error) {
return &PostUsecase{repo: opt.repo}, nil, nil
}
// PostServiceSet PostServiceSet
var PostServiceSet = wire.NewSet(
wire.Struct(new(PostService), "*"),
wire.Value(A(10)),
wire.Value(B(10)),
wire.InterfaceValue(new(io.Reader), os.Stdin),
// for usecase
wire.Bind(new(IPostUsecase), new(*PostUsecase)),
wire.Struct(new(PostUsecaseOption), "*"),
NewPostUsecase,
NewPostRepo,
)
項目目錄結構
.
├── api
├── cmd
│ └── app
│ ├── main.go
│ ├── wire.go
│ └── wire_gen.go
└── internal
├── domain
│ └── post.go
├── repo
│ └── repo.go
├── service
│ └── service.go
├── usecase
│ └── usecase.go
└── wire_set.go
-
一般在 cmd/xxx 目錄下創建
wire.go
用於構建injector
函數簽名,因爲我們一般會在main
當中構建依賴關係完成服務啓動 -
在 internal 或者是 internal/app 目錄下創建
wire_set.go
構建ProviderSet
,這裏要注意 -
這裏的
ProviderSet
中的Provider
函數只能是當前目錄下創建的 Provider 函數 -
例如可能存在 usecase 和 repo 都依賴 config 如果 repo 創建一個 ProviderSet 包含
NewConfig
,usecase 也來一個,就會導致在wire .
生成代碼的時候報錯,因爲有衝突,同一個組件有兩個 Provider
總結
本文詳細的介紹了 wire 的使用方法和一些存在的坑,避免大家重複踩坑,同時結合上一篇文章當中的項目結構給出了一種實踐方式。依賴注入這個東西如果只是一個比較簡單的應用並且這個應用的開發同學比較少,可以不用引入,引入依賴注入的框架還是會帶來一些複雜性和學習成本,但是如果這個項目有很多同學在協作開發,並且部門要求的依賴組件比較多的時候還是需要引入的,隨着項目代碼的膨脹會導致後面依賴管理的管理越來越複雜,如果想要做一點點重構會帶來很多麻煩。
參考文獻
-
Go 進階訓練營 - 極客時間
-
GitHub - uber-go/dig: A reflection based dependency injection toolkit for Go.
-
GitHub - google/wire: Compile-time Dependency Injection for Go
-
https://medium.com/@dche423/master-wire-cn-d57de86caa1b
-
Golang 依賴注入框架 wire 全攻略
-
Compile-time Dependency Injection With Go Cloud's Wire - The Go Blog
-
https://github.com/golang/go/wiki/CodeReviewComments#interfaces
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/G67gh7niNPZCLCGAat4iNg