---
title: "如何用 Golang 的 wire 做依賴注入"
date: 2023-11-15T00:00:00+08:00
publishDate: 2023-11-15T00:00:00+08:00
lastmod: 2023-11-15T09:42:21+08:00
tags: ["Golang","Clean Architecture","Dependency Injection","經驗","心得"]
toc: true
permalink: "https://blog.aotoki.me/posts/2023/11/15/how-to-use-wire-dependency-injection/"
language: "zh-tw"
---


[google/wire](https://github.com/google/wire) 是一個依賴注入（Dependency Injection）的工具，透過程式碼生成（Code Generate）來幫助我們解決 Golang 中一個物件對另一個物件有依賴關係時，需要事先產生的問題。

> 在開始這篇之前，也建議閱讀[從 wire 學到依賴注入沒有講的事](https://blog.aotoki.me/posts/2023/06/21/learn-dependency-injection-from-google-wire/)了解一些基本的概念。

<!--more-->

## 釐清依賴{#clarify-dependency}

所謂的依賴，表示一個類別（Class）需要明確知道另一個類別的存在才能夠順利運作。不過，我們可以透過一些抽象化的方式來處理，例如定義介面（Interface）來解除這樣的直接依賴關係。

舉例來說，我們有一個錢包物件需要知道金錢物件的存在才能夠運行，那麼就構成物件之間的依賴。

```go
type Wallet struct {
  id      string
  balance Money
}

type Money struct {
  currency string
  amount   int
}
```

在 [Clean Architecture](https://www.tenlong.com.tw/products/9789864342945) 這本書中對這類情況的描述，指的是需要一起打包（Package）的情況，無法透過替換套件來抽換元件（Component）

在 Golang 中是以套件（Package）來進行元件的區分，因此當我們從一個套件引用另一個套件時，就構成了這種依賴。

```go
// package controller
import (
  // ...
  "example/entity"
)

func GetWallet(res http.ResponseWriter, req *http.Request) {
  // ...
  wallet := entity.NewWallet("demo")
  // ...
}

// package entity
type Wallet struct {}
```

上述的範例中，套件 `controller` 對套件 `entity` 有依賴關係，表示我們需要在編譯（Compile）時，有 `entity` 套件的存在才能夠順利編譯。

> 切分成套件已經有一定程度的元件化，然而這種依賴關係仍然非常緊密仍有較高的耦合度。

要處理乾淨 wire 的依賴注入，就需要區分出哪些套件需要「可以抽換」或者「容易擴充」然後再做進一步的處理，並不一定要將所有依賴關係切割到非常乾淨。

## 確立邊界{#define-boundary}

邊界（Boundary）在這邊指的是將物件之間的職責（Responsibility）分組後，自然形成的界線。舉例來說，我們透過 `Controller` 這類物件處理 HTTP 請求，並且利用 `Model` 這類物件處理狀態的保存和更新，那麼 `Controller` 和 `Model` 因為職責的差異自然形成了邊界。

邊界會因為處理的方式有不一樣的耦合程度，例如：我們需要直接知道某個物件如何使用，耦合程度就會比較高。

```go
// package controller
import (
  // ...
  "example/entity"
)

func GetWallet(res http.ResponseWriter, req *http.Request) {
  // ...
  wallet := entity.NewWallet("demo")

  res.Write([]byte(wallet.GetCurrency())
}

// package entity
type Wallet struct {
  id      string
  balance Money
}

func(w *Wallet) GetCurrency() string {
  return w.balance.currency
}
```

然而，我們可以透過介面（Interface）的方式，來將兩個物件耦合關係降低變得鬆散。

```go
package controller

type Wallet interface {
  GetCurrency() string
}

type WalletLoader interface {
  Find(id string) Wallet
}

func GetWallet(res http.ResponseWriter, req *http.Request) {
  // ...
  wallet := loader.Find(id)

  res.Write([]byte(wallet.GetCurrency())
}
```

上述範例定義了 `WalletLoader` 介面，要求一個 `GetCurrency()` 方法回傳 `Wallet` 物件，此時我們就不需要明確 `import "example/entity"` 去依賴 `entity` 這個套件，只要符合條件的物件傳入，就能夠運行。

## 分組{#grouping}

會需要使用 wire 的情境，依賴關係通常相對複雜。需要區分出一個「抽換」的單位，將不同的元件根據我們的需要劃分成幾個不同的模組。

以 wire 的範例 [guestbook](https://github.com/google/go-cloud/tree/master/samples/guestbook) 為例子，除了能運行在 Google Cloud Platform 之外，也能在 Amazon Web Service 或者 Azure 上運行，那麼至少就會分成兩個組別。

* Application
* (Cloud) Infrastructure

一個是 Gustbook 的實作，這些實作並不會知道背後的資料庫、檔案是怎麼保存的，根據專案的需求會需要區分這些資訊出來，通常還會再更細一些。

使用 wire 的最終目標，是要讓我們在運行應用時可以很簡單的用 `initApp()` 方式來初始化整個應用，而不需要依照不同環境、需求去撰寫相對應的程式碼。

也就是說，要支援不同雲端的實作，目標是最後能得到類似這樣的 `main()` 函式：

```go
func main() {
  cloudType := os.Getenv("CLOUD_TYPE")
  ctx := context.Background()

  var srv *server.Server
  switch cloudType {
    case "aws":
      srv = setupAws(ctx)
    case "gcp":
      srv = setupGcp(ctx)
    case "azure":
	  srv = setupAzure(ctx)
	default:
		panic("unsupport cloud")
  }

  srv.Run()
}
```

## 實作{#use-wire}

接下來我們用 `setupAws()` 這個函式來作為例子，一步一步分解需要實作的部分。假設我們有一個以 ID 來查詢錢包的服務，在 AWS 上我們會用 DynamoDB 來保存資料。

我們先用比較簡單的方式區分成 `Controller`、`Repository`、`Entity` 三種物件，分別的實作（部分）如下：

```go
package controller

import (
  // ...
  "example/entity"
)

type WalletRepository struct {
  Find(ctx context.Context, id string) (*entity.Wallet, error)
  Save(ctx context.Context, *entity.Wallet) error
}

type Wallet struct {
  wallets WalletRepository
}

func (ctrl *Wallet) Get(res http.ResponseWriter, req *http.Request) {
  // ...
  wallet := ctrl.wallets.Find(id)
  // ...
}

func (ctrl *Wallet) Update(res http.ResponseWriter, req *http.Request) {
  // ...
}
```

```go
package entity

type Wallet struct {
  // ...
}

// ...
```

```go
package repository

import (
  // ...
  "example/entity"
  "example/controller"
)

var _ controller.WalletRepository = &DynamoDbWallet{}

type DynamoDbWallet struct {
  db *dynamodb.Client
}

func NewDynamoDbWallet(client *dynamodb.Client) *DynamoDbWallet {
  return &DynamoDbWallet {
    db: client,
  }
}

func (r *DynamoDbWallet) Find(id string) (*entity.Wallet, error) {
  // ...
}

func (r *DynamoDbWallet) Save(wallet *entity.Wallet) error {
  // ...
}
```

在上述的例子中，依賴關係大致上如下：

* `Controller` 依賴 `Entity`
* `Repository` 依賴 `Controller` 和 `Entity`

對 `Controller` 來說，他並不在意是怎樣的物件提供 `Find()` 和 `Save()` 方法，在定義介面時會由 `Controller` 來描述他的「需求」至於怎麼滿足則是依賴的物件要去思考的，實作 `Repository` 時我們可以引用 `Controller` 套件來檢查是否有滿足依賴。

除了 Application 的依賴之外，我們還需要滿足 AWS 上的依賴，在 [aws-sdk-go-v2](https://github.com/aws/aws-sdk-go-v2) 的情境，會有 `dynamodb.Client` 依賴 `aws.Config` 的需要。

將 `setupAws()` 串起來，就會需要一系列如下的依賴注入：

* 注入 `DynamoDbWallet` 給 `Controller`  滿足 `WalletRepository` 介面
* 注入 `dynamodb.Client` 給 `DynamoDbWallet`
* 注入 `aws.Config` 給 `dynamodb.Client`

在 wire 用於 Code Generate 的檔案（通常會叫 `wire.go` 但沒有硬性限制）需要實作以下內容：

```go
package main

import (
  // ...
)

func setupAws(ctx context.Context) (*server.Server, error) {
  wire.Build(
    awsConfig,
    awsDynamoDb
    awsRepositorySet,
    server.New,
  )

  return nil, nil
}

func awsConfig(ctx context.Context) (aws.Config, error) {
  return config.LoadDefaultConfig(ctx)
}

func awsDymamoDb(cfg aws.Config) *dynamodb.Client {
  return dynamodb.NewFromConfig(cfg)
}

var awsRepositorySet = wire.NewSet(
  repository.NewDynamoDbWallet,
  wire.Bind(new(controller.WalletRepository), new(*repository.DynamoDbWallet))
)
```

上面的例子，我們可以直接將 `config.LoadDefaultConfig` 放到 `wire.Build` 中也能夠順利運作，然而 `dynamodb.NewFromConfig` 就無法這樣使用，因此使用自訂的函式來處理會更加適合。

這是 wire 在處理注入時，會檢查所有傳入的參數（Parameter）都有被提供，在 `dynamodb.NewFromConfig` 這個方法還有額外的 Rest Parameter，可以提供 `dynamodb.Option` 選項來調整 DynamoDB 的設定，如果直接放到 `wire.Build` 就會出現找不到 `[]dynamodb.Option` 的錯誤。

同時，我們要提供的 Repository 都是依賴於 AWS 的，可以直接製作一個 `awsRepositorySet` 來提供相關的設定。另外，對 wire 來說介面的綁定是無法自己判斷符合哪一個介面，因此需要使用 `wire.Bind` 來做關係的定義。

> 我們也可以將 `awsRepositorySet` 放到 Repository 裡面，以 `repository.AwsRepositorySet` 的方式提供，也更好將相關邏輯統整在一起

除此之外，我們還可以做一些變化，假設我們想要對 Repository 增加一層 Cache 支援時，可以將 `awsRepositorySet` 改為這樣的設計。

```go
var awsRepositorySet = wire.NewSet(
  cacheableAwsWalletRepository,
)

func cacheableAwsWalletRepository(client *dynamodb.Client, cfg *config.Config) controller.WalletRepository {
  repo := repository.NewDynamoDbWallet(client)

  if cfg.Cache.Enabled {
    return repository.NewCachedWallet(repo)
  }

  return repo
}
```

因為明確的定義回傳的是符合 `controller.WalletRepository` 的介面，就不需要透過 `wire.Bind` 額外處理，使用獨立的函式也能夠讓我們根據設定微調產生的 Repository 來決定是否提供快取功能。

透過這樣的方式，我們就可以讓 wire 協助我們將所有運行所需的物件一口氣初始化，而不需要每個物件都手動建立，也能確保在編譯階段就能發現是否有依賴缺少或者被改變的狀況。

> 如果要將 `cacheableAwsWalletRepository` 放到 Repository 套件，就需要改為 `CacheableAwsWalletRepository` 的公開方法，使用上還是會遵照 Golang 的原則，無法呼叫其他套件的私有方法。

