Go中使用Google Wire實現依賴注入

banq發表於2024-04-19

關注點分離、鬆散耦合系統和依賴倒置原則等都是軟體工程中眾所周知的概念,並且在建立良好的計算機程式的過程中非常重要。在本文中,我們將討論一種完全應用這三個原則的技術,稱為依賴注入。

Wire是 Go 中用於依賴注入的程式碼生成器。Wire 會為我們生成必要的初始化程式碼。我們只需要定義提供者和注入者。提供者是普通的 Go 函式,根據給定的依賴關係提供值,而注入器是按依賴順序呼叫提供者的函式。

案例:
考慮一下,我們正在開發為使用者註冊提供端點的 HTTP 伺服器。儘管只有一個端點,但它設計為三層,通常出現在更復雜的應用程式中:儲存庫、用例和控制器。

假設有以下目錄結構,

.
├── go.modgo.mod
├── go.sum
├── internal
│   ├── domain
│   │   ├── model
│   │   │   └── user.go
│   │   └── repository
│   │       └── user.go
│   ├── handler
│   │   └── handler.go
│   ├── interface
│   │   └── datastore
│   │       └── user.go
│   └── usecase
│       ├── request
│       │   └── user.go
│       ├── user
│       │   └── user.go
│       └── user.go
└── main.go

現在,讓我們在 internal/interface/datastore/user.go 中定義第一個提供程式。在下面的程式碼片段中,New 是一個提供程式函式,它將 *sql.DB 作為依賴關係,並返回 Repository 的具體實現。

<font>// internal/interface/datastore/user.go<i>
package datastore

import (
   
"context"
   
"database/sql"
   
"inject/internal/domain/model"
)

type Repository struct {
    db *sql.DB
}

func New(db *sql.DB) *Repository {
    return &Repository{db: db}
}

func (r Repository) Create(ctx context.Context, user model.User) error {
   
// TODO: implement me<i>
    return nil
}

Usecase 層將透過抽象或介面使用 Repository 的具體實現。換句話說,我們為 Usecase 層提供的功能依賴於介面,而不是 Repository 的具體實現。

從技術上講,這個介面應由消費層擁有,但我個人認為,這並不一定意味著它們都應存在於同一個包中。在我們的例子中,Usecase 的提供者和 Repository 的介面分別定義在 internal/usecase/user/user.go 和 internal/domain/repository/user.go 中。

<font>// internal/usecase/user/user.go<i>
package user

import (
   
"context"
   
"inject/internal/domain/repository"
   
"inject/internal/usecase/request"
)

type Usecase struct {
    repository repository.Repository
}

func New(repository repository.Repository) *Usecase {
    return &Usecase{repository: repository}
}

func (u Usecase) Create(ctx context.Context, req request.CreateUserRequest) error {
   
// TODO: implement me<i>
    return nil
}

與前面的 Repository 提供程式一樣,我們這裡的 Usecase 提供程式也會返回一個具體實現。

<font>// internal/domain/repository/user.go<i>
package repository

import (
   
"context"
   
"inject/internal/domain/model"
)

type Repository interface {
    Create(ctx context.Context, user model.User) error
}

最後,Usecase 的具體實現也將透過抽象或介面被 Controller 使用。Controller 的提供程式和 Usecase 的介面分別在 internal/handler/handler.go 和 internal/usecase/user.go 中定義,具體如下

<font>// internal/interface/datastore/user.go<i>
package handler

import (
   
"inject/internal/usecase"
   
"net/http"
)

type Handler struct {
    usecase usecase.Usecase
}

func New(usecase usecase.Usecase) *Handler {
    return &Handler{usecase: usecase}
}

func (h Handler) Create() http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
       
// TODO: implement me<i>
        w.WriteHeader(http.StatusOK)
    }
}

// internal/usecase/user.go<i>
package usecase

import (
   
"context"
   
"inject/internal/usecase/request"
)

type Usecase interface {
    Create(ctx context.Context, req request.CreateUserRequest) error
}

現在,所有必要的提供程式都已完成,我們可以在 main.go 中手動執行依賴注入,如下所示

<font>// main.go<i>
package main

import (
   
"database/sql"
   
"log"
   
"net/http"

   
"inject/internal/handler"
   
"inject/internal/interface/datastore"
   
"inject/internal/usecase/user"

    _
"github.com/go-sql-driver/mysql"
)

func main() {
    db, err := sql.Open(
"mysql", "dataSourceName")
    if err != nil {
        log.Fatalf(
"sql.Open: %v", err)
    }

    repository := datastore.New(db)
    usecase := user.New(repository)
    handler := handler.New(usecase)

    mux := http.NewServeMux()
    mux.HandleFunc(
"POST /users", handler.Create())

    log.Fatalf(
"http.ListenAndServe: %v", http.ListenAndServe(":8000", mux))
}

接下來,如何使用 Wire 生成類似上述的初始化程式碼?

使用 Wire
透過 Wire,我們打算讓最終的 main.go 看起來更簡潔,就像這樣

<font>// main.go<i>
package main

import (
   
"log"
   
"net/http"
)

func main() {
    handler, err := InitializeHandler()
    if err != nil {
        log.Fatal(err)
    }

    log.Fatal(http.ListenAndServe(
":8000", handler))
}

我們可以先建立一個檔案,通常命名為 wire.go。它可以定義在一個單獨的軟體包中,但在本例中,我們將把它定義在專案的根目錄下。但在建立 wire.go 之前,最好先重構一下之前的部分程式碼,尤其是在建立資料庫連線例項和註冊 API 路由時。下面的新提供程式就能實現這一目的、

<font>// pkg/mysql/mysql.go<i>
package mysql

import (
   
"database/sql"

    _
"github.com/go-sql-driver/mysql"
)

func New() (*sql.DB, error) {
    db, err := sql.Open(
"mysql", "dataSourceName")
    if err != nil {
        return nil, err
    }
    return db, nil
}

// internal/handler/route.go<i>
package handler

import
"net/http"

func Register(handler *Handler) *http.ServeMux {
    mux := http.NewServeMux()
    mux.HandleFunc(
"POST /users", handler.Create())
    return mux
}

上述提供者函式 Register 接受處理程式的具體實現。當然,也可以使用抽象或介面。但我們將保持原樣,就像我們讓 Repository 的提供函式接受 *sql.DB 型別的具體實現一樣。這與前面提到的依賴反轉原則並不矛盾。事實上,這也許是一個很好的例子,說明如果沒有直接原因,我們不必在程式碼中建立抽象。

好了,讓我們回到我們的 wire.go。根據我們簡化後的 main.go 檔案,你可能已經意識到 InitializeHandler 函式可能是由 Wire 生成的--是的,沒錯!為了正確生成這個函式,我們可以這樣編寫 wire.go

<font>//go:build wireinject<i>
// +build wireinject<i>

package main

import (
   
"net/http"
  
   
"inject/internal/domain/repository"
   
"inject/internal/handler"
   
"inject/internal/interface/datastore"
   
"inject/internal/usecase"
   
"inject/internal/usecase/user"
   
"inject/pkg/mysql"

   
"github.com/google/wire"
)

func InitializeHandler() (*http.ServeMux, error) {
    wire.Build(
        mysql.New,
        datastore.New,
        wire.Bind(new(repository.Repository), new(*datastore.Repository)),
        user.New,
        wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
        handler.New,
        handler.Register,
    )
    return &http.ServeMux{}, nil
}

基本上,在 wire.go 中,我們會告訴 Wire 有關注入函式 InitializeHandler 的模板。它返回 *http.ServeMux 和錯誤。請注意,返回值 (&http.ServeMux{}, nil) 只是為了滿足編譯器的要求。為了正確返回所需值,我們在 Build 函式中宣告瞭所有必要的提供程式:mysql.New、datastore.New、user.New、handler.New 和 handler.Register。

儘管 Wire 足夠聰明,能識別出依賴關係圖,但它仍然需要被明確告知某些具體實現滿足某些介面。請記住,datastore.New 和 user.New 返回的具體實現型別為 *datastore.Repository 和 *user.Usecase,它們分別滿足 repository.Repository 和 usecase.Usecase 介面。這兩種情況所需的顯式宣告都是透過 Bind 函式實現的。

請注意,我們需要將 wire.go 排除在最終二進位制檔案之外。這可以透過在 wire.go 檔案頂部新增構建約束來實現。

接下來,我們可以在應用程式的根目錄下呼叫 wire 命令

wire

如果之前沒有安裝 Wire,請先執行以下命令

go install github.com/google/wire/cmd/wire@latest

該 wire 命令將生成一個名為 wire_gen.go 的檔案,其內容是 InitializeHandler 函式的生成程式碼,如下所示

<font>// Code generated by Wire. DO NOT EDIT.<i>

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

package wire

import (
   
"inject/internal/handler"
   
"inject/internal/interface/datastore"
   
"inject/internal/usecase/user"
   
"inject/pkg/mysql"
   
"net/http"
)

// Injectors from wire.go:<i>

//go:generate wire<i>
func InitializeHandler() (*http.ServeMux, error) {
    db, err := mysql.New()
    if err != nil {
        return nil, err
    }
    repository := datastore.New(db)
    usecase := user.New(repository)
    handlerHandler := handler.New(usecase)
    serveMux := handler.Register(handlerHandler)
    return serveMux, nil
}

生成的初始化器程式碼與我們在第一版 main.go 中編寫的程式碼基本相同。

修改依賴關係
比方說,我們想修改 mysql.New 提供程式以接受配置結構體,因為我們不想直接在其中硬編碼資料來源名稱--這種做法通常被認為是不好的。為此,我們建立了一個特殊目錄來儲存應用程式配置檔案,並建立了一個新的提供程式來讀取檔案並返回 config 結構。我們最終的目錄結構將如下所示:

.
├── config
│   ├── config.gogo
│   └── file
│       └── config.json
├── go.mod
├── go.sum
├── internal
│   ├── domain
│   │   ├── model
│   │   │   └── user.go
│   │   └── repository
│   │       └── user.go
│   ├── handler
│   │   ├── handler.go
│   │   └── route.go
│   ├── interface
│   │   └── datastore
│   │       └── user.go
│   └── usecase
│       ├── request
│       │   └── user.go
│       ├── user
│       │   └── user.go
│       └── user.go
├── main.go
├── pkg
│   └── mysql
│       └── mysql.go
├── wire_gen.go
└── wire.go

在 config/config.go 中,我們定義了 Config 結構及其提供者:

package config

type Config struct {
    DatabaseDSN string
    AppPort     string
}

func Load() (Config, error) {
    <font>// TODO: implement me<i>
    return Config{}, nil
}

接下來,我們要做的就是把這個新的提供程式新增到 wire.go 檔案中。是的,你說得沒錯,把它作為 Build 函式管道的一部分插入、

<font>//go:build wireinject<i>
// +build wireinject<i>

package wire

import (
   
"net/http"

   
"inject/config"
   
"inject/internal/domain/repository"
   
"inject/internal/handler"
   
"inject/internal/interface/datastore"
   
"inject/internal/usecase"
   
"inject/internal/usecase/user"
   
"inject/pkg/mysql"

   
"github.com/google/wire"
)

func InitializeHandler() (*http.ServeMux, error) {
    wire.Build(
        config.Load,
        mysql.New,
        datastore.New,
        wire.Bind(new(repository.Repository), new(*datastore.Repository)),
        user.New,
        wire.Bind(new(usecase.Usecase), new(*user.Usecase)),
        handler.New,
        handler.Register,
    )
    return &http.ServeMux{}, nil
}

再次重新執行 wire 命令--或者這次我們也可以執行 go 生成命令--將告訴 Wire 重新生成初始化程式碼,結果如下

<font>// Code generated by Wire. DO NOT EDIT.<i>

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

package wire

import (
   
"inject/config"
   
"inject/internal/handler"
   
"inject/internal/interface/datastore"
   
"inject/internal/usecase/user"
   
"inject/pkg/mysql"
   
"net/http"
)

// Injectors from wire.go:<i>

func InitializeHandler() (*http.ServeMux, error) {
    configConfig, err := config.Load()
    if err != nil {
        return nil, err
    }
    db, err := mysql.New(configConfig)
    if err != nil {
        return nil, err
    }
    repository := datastore.New(db)
    usecase := user.New(repository)
    handlerHandler := handler.New(usecase)
    serveMux := handler.Register(handlerHandler)
    return serveMux, nil
}

總結
我們介紹了使用 Wire 的一個簡單示例,演示了它如何幫助我們透過依賴注入來構建初始化程式碼。但這並不是 Wire 的全部。事實上,它還有一些其他有用的功能尚未在此討論。要想最大限度地利用 Wire,請查閱此處的文件here.。

相關文章