关注点分离、松耦合系统和依赖反转原则等概念在软件工程中是众所周知的,并且在创建良好的计算机程序过程中至关重要。在本文中,我们将讨论一个同时应用了这三个原则的技术,称为依赖注入。我们将尽可能地实践,更加重点地讨论如何在 Go 应用程序中实现依赖注入。但在进一步讨论之前,让我们重新审视一下,究竟什么是依赖注入?
如前所述,依赖注入 是一种技术,其关注点在于确保想要使用特定服务的对象或函数不必知道如何构造这样的服务,从而将构造对象和使用对象的关注点分离,导致松耦合的程序。接收对象或函数由外部注入器提供其依赖项,而它并不知道这些依赖项。依赖注入还鼓励使用依赖反转原则,即接收对象只需要声明其使用的服务的接口,而不需要它们的具体实现。
Robert van Gen 在他的一篇文章 中指出,虽然依赖注入在小规模上运行良好,但对于具有复杂依赖关系图的更大型应用程序,它将导致大量的初始化代码块。这就是 Wire 等工具非常有用的地方。Wire 是 Go 中用于依赖注入的代码生成器。是的,你猜对了,Wire 将为我们生成必要的初始化代码。我们只需要定义提供程序和注入器。提供程序是普通的 Go 函数,根据它们的依赖项提供值,而注入器是按依赖顺序调用提供程序的函数。为了更好地说明这一点,将呈现一个示例。
设置游乐场
假设我们正在开发一个 HTTP 服务器,为用户注册提供端点。尽管只有一个端点,但它是使用通常出现在更复杂应用程序中的三层设计:存储库、用例和控制器。出于不那么重要的原因,让我们假设它具有以下目录结构,
. ├── go . 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 }
这个 Repository 的具体实现将通过抽象或接口由 UseCase 层使用。换句话说,我们的 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 . U secase ) * 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 上面接受 Handler 的具体实现。当然,也可以使用抽象或接口。但我们将其保留,就像我们让 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 和 error。请注意,(&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 . go │ └── 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 generate 命令 — 将告诉 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,可以参考这里的文档 。