---
title: "wire 的依賴注入 - Clean Architecture in Go"
date: 2025-01-17T00:00:00+08:00
publishDate: 2025-01-17T00:00:00+08:00
lastmod: 2025-04-05T13:52:52+08:00
tags: ["Golang","Clean Architecture","架構","經驗"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/01/17/clean-architecture-in-go-wire-dependency-injection/"
language: "zh-tw"
---


在實踐 Clean Architecture 的過程中，如果有依賴注入工具的協助會容易很多，初期的效果並不明顯，然而隨著系統成長，要建立的物件逐漸變多後，就會發現能夠自動處理依賴、生成物件是非常重要的，在 Golang 可以使用 [google/wire](https://github.com/google/wire) 來協助我們。

<!--more-->

## wire 的特性{#feature-of-wire}

跟常見的依賴注入框架不同的地方在於，wire 選擇在編譯階段就決定好所有依賴，因此我們不會需要在運行時透過反射（Reflect）機制來推斷某個物件所依賴的物件型別，並且動態的生成這個物件。

因為這樣的特性，大多數時候我們能夠在運行測試之前就注意到依賴的問題盡快修正。然而更大的優點則是，我們會發現在大多數情境中，除了領域模型（Domain Model）之外的物件，幾乎是很少需要重新產生的，那麼就能在初始化階段決定好，在記憶體的使用上也會相對的有效率不少，也不需要額外耗費 CPU 來處理反射相關的行為。

當然，這樣的機制會失去一部分的彈性，然而在大多數情境已經非常足夠。

## 基本使用{#basic-usage}

使用 wire 並不複雜，最常見的是對建構物件的方法進行注入，假設我們希望運行一個 HTTP 伺服器，可以定義一個 `wire.go` 來描述 `http.Handler` 如何被生成。

```go
package main
// ...

func initialize() http.Handler {
	wire.Build(
	    usecase.DefaultSet,
	    api.DefaultSet
	)

	return nil
}
```

呼叫 `wire .` 找到 `main` 套件下的 `wire.go` 後，會自動產生 `wire_gen.go` 這個檔案，此時內容可能會像這樣

```go
package main
// ...

func initialize() http.Handler {
	helloUseCase := usecase.NewHello()
	// ...

	return &api.Handler{
	    HelloUseCase: helloUseCase,
	    // ...
	}
}
```

那麼，在 `main.go` 中，我們就能直接用 `initialize()` 來產生我們預期的 `http.Handler` 而不需要知道 `api.Handler` 這個實作需要哪些物件。

```go
package main
// ...

func main() {
    handler := initialize()
    http.ListenAndServe(":8080", handler)
}
```

那麼，在 `api` 套件中，我們會用 `wire.NewSet` 定義 `api.Handler` 跟 `http.Handler` 的關係。

```go
package api
// ...

var DefaultSet = wire.NewSet(
    wire.Struct(new(Handler), "**")), // 對 Handler 所有 Public Field 注入
    wire.Bind(new(http.Handler), new(*Handler)), // 表示 api.Handler 是 http.Handler 的實作
)

// ...
```

至於 `usecase` 套件，則是使用 Provider 的方式定義。

```go
package usecase
// ...

var DefaultSet = wire.NewSet(
    NewHello, // 定義 Hello{} 由 NewHello() 產生
    // ...
)
```

剩下的處理 `wire` 會自動根據定義產生對應的實作，在經驗上通常只會在 `main` 套件會有 `wire.go` 的存在，其他被引用的套件最多只是提供這個套件會怎麼提供物件給依賴他的物件。

## 測試環境{#testing-environment}

上述我們只討論到生產環境的情境，然而當我們要提供測試替身（Test Double）的時候，就可以利用不同的 Set（組合）來實現這樣的變化。

舉例來說，我們有一個 `usecase.OrderRepository` 的介面存在，用於描述 Use Case 中如何存取訂單。

```go
package usecase
// ...

type OrderRepository interface {
    Find(ctx context.Context, id string) (*entity.Order, error)
    // ...
}
```

為了滿足這個需求，我們會在 `repository` 套件中進行對應的實作，像是 PostgreSQL 的版本。

```go
package repository
// ...

var DefaultSet = wire.NewSet(
    NewPostgresOrders,
    // ...
)

type PostgresOrders struct {
    // ...
}

// ...
```

那麼，假設我們希望在測試環境使用 SQLite 來簡化測試，此時只需要定義一個新的 Set 來處理即可。

```go
package repository
// ...

var SQLiteSet = wire.NewSet(
	NewSQLiteOrders,
	// ...
)
```

在原本的 `wire.go` 中，我們可以另外定義一個 `initializeTest()` 方法，來參考不同的依賴。

```go
package main
// ...

func initializeTest() {
	wire.Build(
		repository.SQLiteSet,
		usecase.DefaultSet,
		api.DefaultSet
	)

	return nil
}
```

這樣產生的物件就會跟 `repository.DefaultSet` 不同，那麼只需要在測試時使用 `initializeTest()` 來取得 `api.Handler` 即可。

在 wire 官方的範例中，這個技巧也可以搭配 Command Line Flags 來做到切換不同雲端實作的機制。

```go
package main
// ...

func main() {
	// ...
	var cloud string
	flag.StringVar(&cloud, "cloud", "aws", "the cloud provider")

	var handler http.Handler
	switch cloud {
	    case "aws":
		    handler = initializeAws()
		case "gcp":
			handler = initializeGcp()
		case "azure":
			handler = initializeAzure()
		default:
			// ...
	}

	http.ListenAndServe(":8080", handler)
}
```

後續的文章不會特別提到 wire 的使用，然而大多數新定義的物件都會需要加入到對應套件的 Set 中來確保 wire 能夠正常運作。

