博客 / 詳情

返回

萬字長文:在 Go 中如何優雅的使用 wire 依賴注入工具提高開發效率?上篇

如果你做過 Java 開發,那麼想必一定聽説或使用過依賴注入。依賴注入是一種軟件設計模式,它允許將組件的依賴項外部化,從而使組件本身更加模塊化和可測試。在 Java 中,依賴注入廣泛應用於各種框架中,幫助開發者解耦代碼和提高應用的靈活性。本文就來介紹下什麼是依賴注入,以及在 Go 語言中如何實踐依賴注入,提高 Go 項目的開發效率和可維護性。

什麼是依賴注入?

正如前文所述,依賴注入(dependency injection,縮寫為 DI)是一種軟件設計模式。

官方定義比較晦澀,我直接舉個例子你就理解了。

在 Web 開發中,我們可以在 store 層(有些地方可會將其命名為 repositoryrepo 等)來操作數據庫進行 CRUD。Go 語言中可以使用 GORM 操作數據庫,所以 store 依賴 *gorm.DB,示例代碼如下:

type userStore struct {
    db *gorm.DB
}

func NewStore() *userStore {
    db := NewDB()
    return &userStore{db: db}
}

func (u *userStore) Create(ctx context.Context, user *model.UserM) error {
    return u.db.Create(&user).Error
}
NOTE: 如果你對 GORM 不太瞭解,可以閲讀我的另一篇文章《Go 語言流行 ORM 框架 GORM 使用介紹》。

針對這一小段示例代碼,我們可以按照如下方式創建一個用户:

store := NewStore()
store.Create(ctx, user)

我們還可以將示例代碼修改成這樣:

type userStore struct {
    db *gorm.DB
}

func NewStore(db *gorm.DB) *userStore {
    return &userStore{db: db}
}

func (u *userStore) Create(ctx context.Context, user *model.UserM) error {
    return u.db.Create(&user).Error
}

修改後示例代碼中,我將 *gorm.DB 對象 db 的實例化過程,移動到了 NewStore 函數外面,在調用 NewStore 創建 *userStore 對象 store 時,將其通過參數形式傳遞進來。

現在,如果要創建一個用户,用法如下:

db := NewDB()
store := NewStore(db)
store.Create(ctx, user)

沒錯,我們已經在使用依賴注入了。

我們還是使用 store.Create(ctx, user) 創建用户。但構造 store 時,*userStore 依賴 *gorm.DB,我們使用構造函數 NewStore 創建 *userStore 對象,並且將它的依賴對象 *gorm.DB 通過函數參數的形式注入進來,這種編程思想,就叫「依賴注入」。

回想一下,我們平時在編寫 Go 代碼的過程中,為了方便測試,是不是經常將某個方法的依賴項通過參數傳遞進來,而非在方法內部實例化,這就是在使用依賴注入編寫代碼。

我在文章《在 Go 中如何編寫出可測試的代碼》中就有提到如何使用依賴注入來解決外部依賴問題,你可以點擊文章進行閲讀。

在 Go 中使用依賴注入的核心目的,就是為了解耦代碼。這樣做的主要好處是:

  1. 方便測試。依賴由外部注入,方便使用 fake object 來替換依賴項。
  2. 每個對象僅需要初始化一次,其他方法都可以複用。比如使用 db := NewDB() 初始化得到一個 *gorm.DB 對象,在 NewUserStore(db) 時可以使用,在 NewPostStore(db) 時還可以使用。
NOTE: 我不太喜歡使用比較官方的話術來講解技術,因為本來技術就需要理解成本,而官方的定義往往晦澀難懂。為了降低讀者的心智負擔,我更喜歡用白話講解。
但説到「依賴注入」,定會有人提及「控制反轉」。為了不一些讓讀者產生困惑,這裏簡單説明下控制反轉和依賴注入的關係:控制反轉(英語:Inversion of Control,縮寫為 IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。其中最常見的方式叫做依賴注入(dependency injection,縮寫為 DI)。
我們可以簡單的將控制反轉理解為一種思想,而依賴注入是這一思想的具體實現方式。

依賴注入工具 Wire 簡介

wire 是一個由 Google 開發的自動依賴注入框架,專門用於 Go 語言。wire 通過代碼生成而非運行時反射來實現依賴注入,這與許多其他語言中的依賴注入框架不同。這種方法使得注入的代碼在編譯時就已經確定,從而提高了性能並保證了代碼的可維護性。

安裝 Wire

wire 分成兩部分,一個是在項目中使用的 Go 包,用於在代碼中引用 wire 代碼;另一個是命令行工具,用於生成依賴注入代碼。

  • 在項目中導入需要先通過 go get 獲取 wire 依賴包。
$ go get -u github.com/google/wire

在 Go 代碼中像其他 Go 包一樣使用:

import "github.com/google/wire"
  • 使用 go install 可以安裝 wire 命令工具。
$ go install github.com/google/wire/cmd/wire

安裝後通過 --help 標誌執行 wire 命令查看其支持的所有子命令:

$ wire --help   
Usage: wire <flags> <subcommand> <subcommand args>

Subcommands:
        check            print any Wire errors found
        commands         list all command names
        diff             output a diff between existing wire_gen.go files and what gen would generate
        flags            describe all known top-level flags
        gen              generate the wire_gen.go file for each package
        help             describe subcommands and their syntax
        show             describe all top-level provider sets

由於絕大多數 wire 子命令不常用,所以這部分會放在本文最後再來講解。

Wire 快速開始

示例程序 main.go 代碼如下:

package main

import "fmt"

type Message string

func NewMessage() Message {
    return Message("Hi there!")
}

type Greeter struct {
    Message Message
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func (g Greeter) Greet() Message {
    return g.Message
}

type Event struct {
    Greeter Greeter
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

示例代碼很好理解,定義了 Message 類型是 string 的類型別名。定義了 Greeter 類型及其構造函數 NewGreeter,並且接收 Message 作為參數,Greeter.Greet 方法會返回 Message 信息。最後還定義了一個 Event 類型,它存儲了 GreeterGreeter 通過構造函數 NewEvent 參數傳遞進來,Event.Start 方法會代理到 Greeter.Greet 方法。

定義如下 main 函數來執行這個示例程序:

func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}

執行示例代碼,得到如下輸出:

$ go run main.go
Hi there!

可以發現,main 函數內部的代碼有着明顯的依賴關係,NewEvent 依賴 NewGreeterNewGreeter 又依賴 NewMessage

NewEvent -> NewGreeter -> NewMessage

我們可以將這部分代碼進行抽離,封裝到 InitializeEvent 函數中,保持入口函數 main 足夠整潔,修改後代碼如下:

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

func main() {
    event := InitializeEvent()
    event.Start()
}

現在是時候讓 wire 登場了,在 main.go 同級目錄創建 wire.go 文件(這是一個約定俗稱的文件命名,不是強制約束):

//go:build wireinject

package main

import (
    "github.com/google/wire"
)

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

我們將 main.go 文件中的 InitializeEvent 函數遷移過來,並且修改了內部邏輯,不再手動調用每個構造函數,而是將它們依次傳遞給 wire.Build 函數,然後使用 return 返回一個空的 Event{} 對象。

現在在當前目錄下執行 wire 命令:

$ wire gen .    
wire: github.com/jianghushinian/blog-go-example/wire/getting-started: wrote /Users/jianghushinian/projects/blog-go-example/wire/getting-started/wire_gen.go

其中:

  • genwire 的子命令,他會掃描指定包中使用了 wire.Build 的代碼,然後為其生成一個 wire_gen.go 的文件。
  • . 表示當前目錄,用於指定包,不指定的話默認就是當前目錄。如果項目下有很多包,可以使用 ./... 表示全部包,這個參數其實跟我們執行 go test 測試時是一個道理。

根據輸出結果可以發現,wire 命令為我們在當前目錄下生成了 wire_gen.go 文件,內容如下:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

神奇的事情發生了,wire 為我們生成了 InitializeEvent 函數的代碼,並且跟我們自己實現的代碼一模一樣。

這就是 wire 的威力,它可以為我們自動生成依賴注入代碼,只需要我們將所有依賴項(這裏是幾個構造函數)傳給 wire.Build 即可。

由於現在當前目錄下存在 3 個 .go 文件:

$ tree    
.
├── go.mod
├── go.sum
├── main.go
├── wire.go
└── wire_gen.go

所以不能再使用 go run main.go 來執行示例代碼了,可以使用 go run . 來執行:

$ go run .
Hi there!

細心的你可能會覺得疑惑🤔,代碼中有兩處 InitializeEvent 函數的定義,程序編譯執行的時候不會報錯嗎?

我們在 wire.go 中定義了 InitializeEvent 函數:

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

然後 wire 命令幫我們在 wire_gen.go 中生成了新的 InitializeEvent 函數:

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

而且這二者都是在同一個包下。

程序沒有編譯報錯,主要取決於 wire.gowire_gen.go 文件中的 //go:build 註釋。

wire.go 文件中,註釋為:

//go:build wireinject

首先 //go:build 叫構建約束(build constraint)或構建標記(build tag),是一個必須放在 .go 文件最開始的註釋代碼。有了它之後,我們就可以告訴 go build 如何來構建代碼。

其次,wireinject 是傳遞給構建約束的選項。選項就相當於一個 if 判斷條件,可以根據選項來定製構建時如何處理 Go 文件。

這個構建約束有兩個作用:

  • 將此文件標記文件為 wire 處理的目標://go:build wireinject 告訴 wire 工具及開發者,該文件包含使用 wire 進行依賴注入的設置。即這通常意味着文件中包含了 wire.Build 函數調用。有了它,文件才會被 wire 識別。
  • 條件編譯:確保在正常的構建過程中,帶有這個構建約束的文件不會被編譯進最終的可執行文件中。它只有在使用 wire 工具生成依賴注入代碼時才被處理。這也是為什麼代碼不會編譯報錯,其實 wire.go 文件只是給 wire 命令用的,go run . 執行的是 main.gowire_gen.go 兩個文件,會忽略 wire.go

注意⚠️://go:build wireinjectpackage main 之間需要保留一個空行,否則程序會報錯。你記住就行,不必過於糾結於此,這個問題在 wire 倉庫的 issues 117 中也有提及。

我們再來看 wire_gen.go 文件,註釋為:

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

第一行註釋僅作為提示用,無特殊用途。

//go:generate 這行註釋是一個 go generate 指令。go generate 是一個由 Go 工具鏈提供的命令,用於在編譯前自動執行生成代碼的命令。這個特定的生成指令告訴 Go 在執行 go generate 命令時,運行 wire 工具來自動生成或更新 wire_gen.go 文件。

go run -mod=mod github.com/google/wire/cmd/wire 這部分指令運行 wire 命令,其中 -mod=mod 確保使用的是項目的 go.mod 文件中指定的依賴版本。

我們也可以驗證下:

$ go generate
wire: github.com/jianghushinian/blog-go-example/wire/getting-started: wrote /Users/jianghushinian/projects/blog-go-example/wire/getting-started/wire_gen.go

執行 go generate 確實會自動執行 wire 命令。

註釋 //go:build !wireinject 同樣是一個構建約束。與 wire.go 中的約束不同,這裏的 !wireinject 多了一個 !! 在編程中通常是取反的意思,所以它用來告訴 wire 忽略此文件。因為這個文件是最終執行的代碼,wire 並不需要知道此文件的存在。

最後一個註釋 // +build !wireinject 其實還是一個構建約束,只不過這是舊版本的條件編譯標記(在 Go 1.17 版本之前使用)。它的作用與 //go:build !wireinject 相同,確保向後兼容性。這意味着在較老的 Go 版本中,編譯條件也能被正確處理。

為什麼要使用依賴注入工具?

前文講解了依賴注入思想,以及通過快速開始的示例程序,我們極速入門了 wire 依賴注入工具的使用。

不過直到到現在我們都還沒有討論過為什麼要使用依賴注入工具?

其實通過前文的示例,我們應該已經體會到,wire 最大的作用就是解放雙手,提高生產力。

示例程序中,依賴鏈只有 3 個對象,一箇中大型項目,依賴對象可能有幾十個,wire 的作用會愈加明顯。

使用依賴注入思想可以有效的解耦代碼,那麼使用依賴注入工具則進一步提高了生產力。我們無需手動實例化所有的依賴對象,僅需要編寫函數聲明,將依賴項扔給 wire.Buildwire 命令就能自動生成代碼,可見 wire 是我們偷懶的利器,畢竟懶才是程序員的第一驅動力 :)。

如果你對依賴注入工具的作用還存在質疑,請接着往下看!

Wire 核心概念

我們已經通過快速開始示例演示了 wire 的核心能力,現在是時候正式介紹下 wire 中的概念了。

在 wire 中,有兩個核心概念:providers(提供者)和 injectors(注入器)。

這兩個概念也很好理解,前文中的 NewEventNewGreeterNewMessage 都是一個 provider。簡單一句話:provider 就是一個可以產生值的函數,這些函數都是普通的 Go 函數

值得注意的是,provider 必須是可導出的函數,即函數名稱首字母大寫。

InitializeEvent 實際上就是一個 injectorinjector 是一個按依賴順序調用 provider 的函數,該函數聲明的主體是對 wire.Build 的調用。使用 wire 時,我們僅需編寫 injector 的簽名,然後由 wire 命令生成函數體。

Wire 高級用法

wire 還有很多高級用法,值得介紹一下。

injector 函數參數和返回錯誤

首先是 injector 函數支持傳參和返回 error

修改示例程序代碼如下:

type Message string

// 接收參數作為消息內容
func NewMessage(phrase string) Message {
    return Message(phrase)
}

type Greeter struct {
    Message Message
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func (g Greeter) Greet() Message {
    return g.Message
}

type Event struct {
    Greeter Greeter
}

// 增加返回錯誤信息
func NewEvent(g Greeter) (Event, error) {
    // 模擬創建 Event 報錯
    if time.Now().Unix()%2 == 0 {
        return Event{}, errors.New("new event error")
    }
    return Event{Greeter: g}, nil
}

func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}

這裏主要修改了兩處代碼,NewMessage 接收一個字符串類型的參數作為消息內容,在 NewEvent 內部模擬了創建 Event 出錯的場景,並將錯誤返回。

現在 InitializeEvent 函數定義如下:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, NewMessage, NewGreeter)
    return Event{}, nil
}

這次傳給 wire.Build 的 3 個構造函數順序不同,可見順序並不重要。但為了代碼可維護性,我建議還是要按照依賴順序依次傳入 provider

這裏返回值增加了 error,所以 return 的值增加了一個 nil,其實我們返回什麼並不重要,只要返回的類型正確即可(確保編譯通過),因為最終生成的代碼返回值是由 wire 生成的。

NOTE: 為了邏輯清晰,我只貼出核心代碼,並且 wire.go 文件也不再貼出 //go:build wireinject 相關代碼,後文也是如此,你在實踐時不要忘記。完整代碼詳見文末給出的 GitHub 地址。

使用 wire 生成代碼如下:

func InitializeEvent(phrase string) (Event, error) {
    message := NewMessage(phrase)
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

現在我們執行示例代碼,可能出現兩種情況:

$ go run .    
Hello World!

或者:

$ go run .      
new event error

使用 ProviderSet 進行分組

wire 為我們提供了 provider sets,顧名思義,它可以包含一組 providers。使用 wire.NewSet 函數可以將多個 provider 添加到一個集合中。

我們把 NewMessageNewGreeter 兩個構造函數合併成一個 provider sets

var providerSet wire.ProviderSet = wire.NewSet(NewMessage, NewGreeter)

wire.NewSet 接收不定長參數,並將它們組裝成一個 wire.ProviderSet 類型返回。

wire.Build 可以直接接收 wire.ProviderSet 類型,現在我們只需要給它傳遞兩個參數即可:

func InitializeEvent(phrase string) (Event, error) {
    wire.Build(NewEvent, providerSet)
    return Event{}, nil
}

使用 wire 生成代碼如下:

func InitializeEvent(phrase string) (Event, error) {
    message := NewMessage(phrase)
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

與之前生成的代碼一模一樣。

分組後,代碼會更加清晰,每個 provider sets 僅包含一組關聯的 providers,下文中實踐部分你還能夠看到 provider sets 更具有意義的用法。

使用 Struct 定製 Provider

有時候一個 struct 比較簡單,我們通常不會為其定義一個構造函數,此時我們可以使用 wire.Struct 作為 provider

為了演示此功能,我們修改 Message 定義如下:

type Message struct {
    Content string
    Code    int
}

修改 InitializeEvent 代碼如下:

func InitializeEvent(phrase string, code int) (Event, error) {
    wire.Build(NewEvent, NewGreeter, wire.Struct(new(Message), "Content"))
    return Event{}, nil
}

這裏使用 wire.Struct(new(Message), "Content") 替代了原來的 NewMessage 作為一個 provider

wire.Struct 函數簽名如下:

func Struct(structType interface{}, fieldNames ...string) StructProvider

structType 就是我們要使用的 structfieldNames 用來控制哪些字段會被賦值。

正常來説 InitializeEventphrase 參數會傳給 MessageContent 字段,code 參數會傳給 Code。wire 根據參數類型來判斷應該將參數傳給誰。

由於我們在這裏僅顯式指定了 Content 字段,所以最終只有 Content 字段會被賦值。

使用 wire 生成代碼如下:

func InitializeEvent(phrase string, code int) (Event, error) {
    message := Message{
        Content: phrase,
    }
    greeter := NewGreeter(message)
    event, err := NewEvent(greeter)
    if err != nil {
        return Event{}, err
    }
    return event, nil
}

從生成的代碼可以發現,確實沒給 Code 賦值。

如果我們想給 Code 賦值,可以這樣寫:

wire.Build(NewEvent, NewGreeter, wire.Struct(new(Message), "Content", "Code"))

也可以這樣寫:

wire.Build(NewEvent, NewGreeter, wire.Struct(new(Message), "*"))

* 表示通配符,即使用 Message 所有字段。

使用 wire.Struct 的好處是少定義一個構造函數,並且可以定製使用字段。

使用 Struct 字段作為 Provider

我們還可以指定 struct 的具體某個字段作為一個 provider

這需要用到 wire.FieldsOf,函數簽名如下:

func FieldsOf(structType interface{}, fieldNames ...string) StructFields

可以發現它跟 wire.Struct 函數參數一樣,區別是返回值不同。

現在示例代碼如下:

type Content string

type Message struct {
    Content Content
    Code    int
}

// NewMessage 注意,這裏返回的是指針類型
func NewMessage(content string, code int) *Message {
    return &Message{
        Content: Content(content),
        Code:    code,
    }
}

MessageContent 字段修改為 string 的別名類型 Content這是有用意的,稍後講解

為了演示更多種情況,這裏採用日常開發中更加常用的場景,即構造函數返回 struct 的指針類型。

修改 InitializeEvent 代碼如下:

func InitializeMessage(phrase string, code int) Content {
    wire.Build(NewMessage, wire.FieldsOf(new(*Message), "Content"))
    return Content("")
}

因為示例代碼中 NewMessage 返回 *Message 類型,而非 Message 類型,所以傳遞給 wire.FieldsOf 必須是 new(*Message) 而不是 new(Message)

InitializeMessage 函數返回 Content 類型,而非 Message 類型。

使用 wire 生成代碼如下:

func InitializeMessage(phrase string, code int) Content {
    message := NewMessage(phrase, code)
    content := message.Content
    return content
}

根據生成的代碼可以發現,在通過 NewMessage 函數創建 *Message 對象以後,會將 message.Content 字段提取出來並返回。

前文在講解 使用 Struct 定製 Provider時我提到過「wire 根據參數類型來判斷應該將參數傳給誰」。

其實不僅僅是參數,wire 規定 injector 函數的參數和返回值類型都必須唯一。不然 wire 無法對應上哪個值該給誰,這也是為什麼我專門定義了 Content 類型作為 Message 的字段,因為 InitializeMessage 的參數 phrase 已經是 string 類型了,所以其返回值就不能是 string 類型了。

現在就來演示一下 injector 函數的參數和返回值類型出現重複的情況,我們可以嘗試把 Message 改回去:

type Message struct {
    Content string
    Code    int
}

// NewMessage 注意,這裏返回的是指針類型
func NewMessage(content string, code int) *Message {
    return &Message{
        Content: content,
        Code:    code,
    }
}

InitializeEvent 函數返回值也改為 string

func InitializeMessage(phrase string, code int) string {
    wire.Build(NewMessage, wire.FieldsOf(new(*Message), "Content"))
    return ""
}

現在使用 wire 生成代碼,會得到類似如下錯誤:

$ wire gen .
wire: wire.go:10:2: multiple bindings for string
        current:
        <- wire.FieldsOf (structfields.go:8:2)
        previous:
        <- argument phrase to injector function InitializeMessage (wire.go:7:1)
wire: github.com/jianghushinian/blog-go-example/wire/getting-started/advanced/structfields: generate failed
wire: at least one generate failure
NOTE: 注意,這裏為了展示清晰,我將輸出的文件絕對路徑進行了修改,去掉了路徑部分,只保留了文件名,不影響輸出語義。後文中可能也會如此。

根據錯誤信息 multiple bindings for string 可知,wire 不支持函數的參數和返回值類型出現重複。

所以,當遇到 injector 函數出現參數或返回值類型重複的情況,可以通過給類型定義別名來解決。

綁定「值」作為 Provider

可以直接將一個作為參數傳給 wire.Value 來構造一個 provider

定義 Message

type Message struct {
    Message string
    Code    int
}

我們可以直接實例化這個 struct,然後將其傳給 wire.Value

func InitializeMessage() Message {
    // 假設沒有提供 NewMessage,可以直接綁定值並返回
    wire.Build(wire.Value(Message{
        Message: "Binding Values",
        Code:    1,
    }))
    return Message{}
}

使用 wire 生成代碼如下:

func InitializeMessage() Message {
    message := _wireMessageValue
    return message
}

var (
    _wireMessageValue = Message{
        Message: "Binding Values",
        Code:    1,
    }
)

可以發現,實際上 wire 為我們定義了一個變量,並將這個變量作為 InitializeMessage 函數返回值。

這種拿來即用的方式,提供了非常大的便利。

wire.Value 接收任何值類型,所以不止 struct,一個普通的 intstring 等類型都可以,就交給你自己去嘗試了。

綁定「接口」作為 Provider

provider 依賴項或返回值並不總是,很多時候是一個接口

我們可以接將一個接口作為參數傳給 wire.InterfaceValue 來構造一個 provider

創建一個 Write 函數,它依賴一個 io.Writer 接口:

func Write(w io.Writer, value any) {
    n, err := fmt.Fprintln(w, value)
    fmt.Printf("n: %d, err: %v\n", n, err)
}

wire.Value 用法類似,我們可以使用 wire.InterfaceValue 綁定接口:

func InitializeWriter() io.Writer {
    wire.Build(wire.InterfaceValue(new(io.Writer), os.Stdout))
    return nil
}

使用 wire 生成代碼如下:

func InitializeWriter() io.Writer {
    writer := _wireFileValue
    return writer
}

var (
    _wireFileValue = os.Stdout
)

生成代碼套路跟 wire.Value 沒什麼區別。

綁定結構體到接口

有時候我們可能會寫出如下代碼:

type Message struct {
    Content string
    Code    int
}

type Store interface {
    Save(msg *Message) error
}

type store struct{}

// 確保 store 實現了 Store 接口
var _ Store = (*store)(nil)

func New() *store {
    return &store{}
}

func (s *store) Save(msg *Message) error {
    return nil
}

func SaveMessage(s Store, msg *Message) error {
    fmt.Printf("save message: %+v\n", msg)
    return s.Save(msg)
}

func RunStore(msg *Message) error {
    s := New()
    return SaveMessage(s, msg)
}

Store 接口定義了一個 Save 方法用來保存 Message,定義了 store 結構體,結構體的指針 *store 實現了 Store 接口,所以 store 的構造函數 New 返回 *store

我們還定義了 SaveMessage 方法,它接收兩個參數,分別是 Store 接口以及 *Message

最終定義的 RunStore 方法接收 *Message,並在內部創建 *store,然後將這兩個變量傳給 SaveMessage 保存消息。

假如我們想使用 wire 命令來生成 RunStore 函數,定義如下:

func WireRunStore(msg *Message) error {
    wire.Build(SaveMessage, New)
    return nil
}

使用 wire 生成代碼將得到報錯:

$ wire gen .
wire: wire.go:7:1: inject WireRunStore: no provider found for github.com/jianghushinian/blog-go-example/wire/getting-started/advanced/bindingstruct.Store
        needed by error in provider "SaveMessage" (bindingstruct.go:29:6)
wire: github.com/jianghushinian/blog-go-example/wire/getting-started/advanced/bindingstruct: generate failed
wire: at least one generate failure

這是因為 wire 的構建依靠參數類型,但不支持接口類型。而 SaveMessages Store 參數就是接口。

此時,我們可以使用 wire.Bind 告訴 wire 工具,將一個結構體綁定到接口:

func WireRunStore(msg *Message) error {
    // new(Store) 接口無需使用指針
    wire.Build(SaveMessage, New, wire.Bind(new(Store), new(*store)))
    return nil
}

這樣,wire 就知道 New 創建得到的 *store 類型需要傳遞給 SaveMessages Store 參數了。

使用 wire 生成代碼如下:

func WireRunStore(msg *Message) error {
    bindingstructStore := New()
    error2 := SaveMessage(bindingstructStore, msg)
    return error2
}

沒有問題。

清理函數

有時候我們的函數返回值可能包含一個清理函數,用來釋放資源,示例代碼如下:

func OpenFile(path string) (*os.File, func(), error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, nil, err
    }

    cleanup := func() {
        fmt.Println("cleanup...")
        if err := f.Close(); err != nil {
            fmt.Println(err)
        }
    }

    return f, cleanup, nil
}

func ReadFile(f *os.File) (string, error) {
    b := make([]byte, 1024)
    _, err := f.Read(b)
    if err != nil {
        return "", err
    }
    return string(b), nil
}

OpenFile 函數接收一個文件路徑作為參數,其內部會打開這個文件,並返回文件對象 *os.File。除此以外,還會返回一個清理函數和 error,清理函數內部會調用 f.Close() 關閉文件對象。

ReadFile 函數依賴 *os.File 文件對象,可以讀取並返回其內容。

我們可以定義如下 injector 函數:

func InitializeFile(path string) (*os.File, func(), error) {
    wire.Build(OpenFile)
    return nil, nil, nil
}

使用 wire 生成代碼如下:

func InitializeFile(path string) (*os.File, func(), error) {
    file, cleanup, err := OpenFile(path)
    if err != nil {
        return nil, nil, err
    }
    return file, func() {
        cleanup()
    }, nil
}

可以發現,wire 能夠正確處理這種情況。

不過,wire 規定清理函數簽名只能為 func()。而 InitializeFile 函數的返回值,也是我們工作中使用 wire 的典型場景:injector 函數返回 3 個值,分別是對象、清理函數以及 error

示例代碼使用方式如下:

f, cleanup, err := InitializeFile("testdata/multi.txt")
if err != nil {
    fmt.Println(err)
}
content, err := ReadFile(f)
if err != nil {
    fmt.Println(err)
}
fmt.Println(content)
cleanup()

還有一種情況,假如我們傳遞給的 wire.Build 多個 provider 都存在清理函數,這時候 wire 命名生成的代碼會是什麼樣呢?

這個就當做作業留給你自己去嘗試了。

NOTE: 如果你懶得嘗試🤣,其實我也寫好了例子,你可以點擊 GitHub 地址進行查看。篇幅所限,我就不貼代碼了,感興趣可以點進去查看。

備用注入器語法,給語法加點糖

前文講過,injector 函數返回值並不重要,只要我們寫在 return 後面的返回值類型,跟函數簽名一致即可。因為 wire 會忽略它們,所以上面很多示例返回值我都使用 nil 來替代。

那麼,既然返回值沒什麼用,我們是否可以偷個懶,不寫 return 呢?

答案是可以的,我們可以直接 panic,這樣程序依然可以通過編譯。

示例代碼如下:

type Message string

func NewMessage(phrase string) Message {
    return Message(phrase)
}

這裏直接在 injector 函數中使用 panic 來簡化代碼:

func InitializeMessage(phrase string) Message {
    panic(wire.Build(NewMessage))
}

使用 wire 生成代碼如下:

func InitializeMessage(phrase string) Message {
    message := NewMessage(phrase)
    return message
}

沒有任何問題。

這種方式少寫了一行 return,算是 wire 給我們提供的一個“語法糖”。Kratos 框架文檔中也是這麼寫的,你可以點擊查看。

至於到底選用 return 還是 panic,社區並沒有一致的規範,看個人喜好就好。我目前更喜歡使用 return,畢竟誰都不希望自己程序出現 panic,佔位也不行 :)。

至此,終於將 wire 的常用功能全部講解完畢。

下篇就進入 wire 生產實踐講解了,敬請期待!

延伸閲讀

  • Compile-time Dependency Injection With Go Cloud's Wire: https://go.dev/blog/wire
  • Wire README: https://github.com/google/wire/blob/main/README.md
  • Wire Documentation: https://pkg.go.dev/github.com/google/wire/internal/wire
  • Wire 源碼: https://github.com/google/wire
  • onex usercenter: https://github.com/superproj/onex/tree/master/internal/usercenter
  • Go Dependency Injection with Wire: https://blog.drewolson.org/go-dependency-injection-with-wire/
  • Golang Dependency Injection Using Wire: https://clavinjune.dev/en/blogs/golang-dependency-injection-u...
  • Dependency Injection in Go using Wire: https://www.mohitkhare.com/blog/go-dependency-injection/
  • Wire 依賴注入: https://go-kratos.dev/docs/guide/wire/
  • Dependency Injection with Dig: https://www.jetbrains.com/guide/go/tutorials/dependency_injec...
  • inject Documentation: https://pkg.go.dev/github.com/facebookgo/inject
  • Build Constraints: https://pkg.go.dev/go/build#hdr-Build_Constraints
  • 控制反轉: https://zh.wikipedia.org/wiki/控制反轉
  • 依賴注入: https://zh.wikipedia.org/wiki/依賴注入
  • SOLID (面向對象設計): https://zh.wikipedia.org/wiki/SOLID_(面向對象設計)
  • 設計模式之美 —— 19 | 理論五:控制反轉、依賴反轉、依賴注入,這三者有何區別和聯繫?: https://time.geekbang.org/column/article/177444
  • 本文 GitHub 示例代碼:https://github.com/jianghushinian/blog-go-example/tree/main/wire

聯繫我

  • 公眾號:Go編程世界
  • 微信:jianghushinian
  • 郵箱:jianghushinian007@outlook.com
  • 博客:https://jianghushinian.cn
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.