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