Go中使用Google Wire实现依赖注入

关注点分离、松散耦合系统和依赖倒置原则等都是软件工程中众所周知的概念,并且在创建良好的计算机程序的过程中非常重要。在本文中,我们将讨论一种完全应用这三个原则的技术,称为依赖注入。

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 的具体实现。

// internal/interface/datastore/user.go
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
    return nil
}

Usecase 层将通过抽象或接口使用 Repository 的具体实现。换句话说,我们为 Usecase 层提供的功能依赖于接口,而不是 Repository 的具体实现。

从技术上讲,这个接口应由消费层拥有,但我个人认为,这并不一定意味着它们都应存在于同一个包中。在我们的例子中,Usecase 的提供者和 Repository 的接口分别定义在 internal/usecase/user/user.go 和 internal/domain/repository/user.go 中。

// internal/usecase/user/user.go
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
    return nil
}

与前面的 Repository 提供程序一样,我们这里的 Usecase 提供程序也会返回一个具体实现。

// internal/domain/repository/user.go
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 中定义,具体如下

// internal/interface/datastore/user.go
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
        w.WriteHeader(http.StatusOK)
    }
}

// internal/usecase/user.go
package usecase

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

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

现在,所有必要的提供程序都已完成,我们可以在 main.go 中手动执行依赖注入,如下所示

// main.go
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 看起来更简洁,就像这样

// main.go
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 路由时。下面的新提供程序就能实现这一目的、

// pkg/mysql/mysql.go
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
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

//go:build wireinject
// +build wireinject

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 函数的生成代码,如下所示

// 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 wire

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

// Injectors from wire.go:

//go:generate wire
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) {
    // TODO: implement me
    return Config{}, nil
}

接下来,我们要做的就是把这个新的提供程序添加到 wire.go 文件中。是的,你说得没错,把它作为 Build 函数管道的一部分插入、

//go:build wireinject
// +build wireinject

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 重新生成初始化代码,结果如下

// 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 wire

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

// Injectors from wire.go:

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.。