控制反转和依赖注入
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI)。依赖注入是生成灵活和松散耦合代码的标准技术,通过明确地向组件提供它们所需要的所有依赖关系。在 Go 中通常采用将依赖项作为参数传递给构造函数的形式:
构造函数NewBookRepo
在创建BookRepo
时需要从外部将依赖项db
作为参数传入,我们在NewBookRepo
中无需关注db
的创建逻辑,实现了代码解耦。
// NewBookRepo 创建BookRepo的构造函数
func NewBookRepo(db *gorm.DB) *BookRepo {
return &BookRepo{db: db}
}
区别于控制反转,如果在NewBookRepo
函数中自行创建相关依赖,这将导致代码高度耦合并且难以维护和调试。
// NewBookRepo 创建BookRepo的构造函数
func NewBookRepo() *BookRepo {
db, _ := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
return &BookRepo{db: db}
}
为什么需要依赖注入工具
现在我们已经知道了应该在开发中尽可能地使用控制反转和依赖注入将程序解耦开来,从而写出灵活和易测试的程序。
在小型应用程序中,我们可以自行创建依赖并手动注入。但是在一个大型应用程序中,手动去实现所有依赖的创建和注入就会比较繁琐。
例如,在一些常见的HTTP服务中,会根据业务需要划分出不同的代码层:
├── internal
│ ├── conf
│ │ └── conf.go
│ ├── data
│ │ └── data.go
│ ├── server
│ │ └── server.go
│ └── service
│ └── service.go
└── main.go
我们的服务需要有一个配置,指定工作模式、连接的数据库和监听端口等信息。
// conf/conf.go
// NewDefaultConfig 返回默认配置,不需要依赖
func NewDefaultConfig() *Config {...}
我们这里定义了一个默认配置,当然后续可以支持从配置文件或环境变量读取配置信息。
在程序的data
层,需要定义一个连接数据库的函数,它依赖上面定义的Config
并返回一个*gorm.DB
(这里使用gorm连接数据库)。
// data/data.go
// NewDB 返回数据库连接对象
func NewDB(cfg *conf.Config) (*gorm.DB, error) {...}
同时定义一个BookRepo
,它有一些数据操作相关的方法。它的构造函数NewBookRepo
依赖*gorm.DB
,并返回一个*BookRepo
。
// data/data.go
type BookRepo struct {
db *gorm.DB
}
func NewBookRepo(db *gorm.DB) *BookRepo {...}
Service
层位于data
层和Server
层的中间,它负责实现对外服务。其中构造函数 NewBookService
依赖Config
和BookRepo
。
// service/service.go
type BookService struct {
config *conf.Config
repo *data.BookRepo
}
func NewBookService(cfg *conf.Config, repo *data.BookRepo) *BookService {...}
server
层又有一个NewServer
构造函数,它依赖外部传入Config
和BookService
。
// server/server.go
type Server struct {
config *conf.Config
service *service.BookService
}
func NewServer(cfg *conf.Config, srv *service.BookService) *Server {...}
在main.go
文件中又依赖Server
创建一个app
。
// main.go
type Server interface {
Run()
}
type App struct {
server Server
}
func newApp(server Server) *App {...}
由于在程序中定义了大量需要依赖注入的构造函数,程序的main
函数中会出现以下情形。所有依赖的创建和顺序都需要手动维护。
// main.go
func main() {
cfg := conf.NewDefaultConfig()
db, _ := data.NewDB(cfg)
repo := data.NewBookRepo(db)
bookSrv := service.NewBookService(cfg, repo)
server := server.NewServer(cfg, bookSrv)
app := newApp(server)
app.Run()
}
我们确实需要一个工具来解决这类问题。
Wire
Wire
是一个专为依赖注入(Dependency Injection
)设计的代码生成工具,它可以自动生成用于初始化各种依赖关系的代码,从而帮助我们更轻松地管理和注入依赖关系。
Wire 安装
我们可以执行以下命令来安装 Wire
工具:
$ go install github.com/google/wire/cmd/wire@latest
安装之前请确保已将 $GOPATH/bin
添加到环境变量 $PATH
里。
Wire 的基本使用
前置代码准备
虽然我们在前面已经通过 go install
命令安装了 Wire
命令行工具,但在具体项目中,我们仍然需要通过以下命令安装项目所需的 Wire
依赖,以便结合 Wire
工具生成代码:
$ go get github.com/google/wire@latest
接下来,让我们模拟一个简单的 web
博客项目,编写查询文章接口的相关代码,并使用 Wire
工具生成代码。
项目的目录结构如下:
.
├── ioc
│ └── article.go
├── main.go
├── service
│ └── article.go
├── web
│ └── article.go
└── wire.go
首先,我们先定义相关类型与方法,并提供对应的 初始化函数:
- 定义
PostHandler
结构体,创建注册路由的方法RegisterRoutes
和查询文章路由处理的方法GetPostById
以及初始化的函数NewPostHandler
,并且它依赖于IPostService
接口:
type PostHandler struct {
serv service.IPostService
}
func (h *PostHandler) RegisterRoutes(engine *gin.Engine) {
engine.GET("/post/:id", h.GetPostById)
}
func (h *PostHandler) GetPostById(ctx *gin.Context) {
content := h.serv.GetPostById(ctx, ctx.Param("id"))
ctx.String(http.StatusOK, content)
}
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
- 定义
IPostService
接口,并提供了一个具体实现PostService
,接着创建GetPostById
方法,用于处理查询文章的逻辑,然后提供初始化函数NewPostService
,该函数返回IPostService
接口类型:
type IPostService interface {
GetPostById(ctx context.Context, id string) string
}
var _ IPostService = (*PostService)(nil)
type PostService struct {
}
func (s *PostService) GetPostById(ctx context.Context, id string) string {
return "欢迎访问博客"
}
func NewPostService() IPostService {
return &PostService{}
}
- 定义一个初始化
gin.Engine
函数NewGinEngineAndRegisterRoute
,该函数依赖于*handler.PostHandler
类型,函数内部调用相关handler
结构体的方法创建路由:
func NewGinEngineAndRegisterRoute(postHandler *web.PostHandler) *gin.Engine {
engine := gin.Default()
postHandler.RegisterRoutes(engine)
return engine
}
使用 Wire 工具生成代码
前置代码已经准备好了,接下来我们编写核心代码,以便 Wire
工具能生成相应的依赖注入代码。
- 首先我们需要创建一个
wire
的配置文件,通常命名为wire.go
。在这个文件里,我们需要定义一个或者多个注入器函数(Injector
函数,接下来的内容会对其进行解释),以便指引Wire
工具生成代码。
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
在上述代码中,我们定义了一个用于初始化 gin.Engine
的注入器函数,在该函数内部,我们使用了 wire.Build
方法来声明依赖关系,其中包括 PostHandler
、PostService
和 InitGinEngine
作为依赖的构造函数。
wire.Build
的作用是 连接或绑定我们之前定义的所有初始化函数。当我们运行 wire
工具来生成代码时,它就会根据这些依赖关系来自动创建和注入所需的实例。
注意:文件首行必须加上 //go:build wireinject
或 // +build wireinject
(go 1.18
之前的版本使用) 注释,作用是只有在使用 wire
工具时才会编译这部分代码,其他情况下忽略。
- 接下来在
wire.go
文件所处目录下执行wire
命令,生成wire_gen.go
文件,内容如下所示:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"github.com/gin-gonic/gin"
"golang-example/wire/blog/ioc"
"golang-example/wire/blog/service"
"golang-example/wire/blog/web"
)
// Injectors from wire.go:
func InitializeApp() *gin.Engine {
iPostService := service.NewPostService()
postHandler := web.NewPostHandler(iPostService)
engine := ioc.NewGinEngineAndRegisterRoute(postHandler)
return engine
}
生成的代码和我们手写区别不大,当我们的组件很多,依赖关系复杂的时候,我们才会感觉到 Wire
工具的好处。
Wire 的核心概念
Wire
有两个核心概念:提供者(providers
)和注入器(injectors
)。
Wire 提供者(providers)
提供者:一个可以产生值的函数,也就是有返回值的函数。例如入门代码里的 NewPostHandler
函数:
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
返回值不仅限于一个,如果有需要的话,可以额外添加一个 error
的返回值。
如果提供者过多的时候,我们还可以以分组的形式进行连接,例如将 post
相关的 handler
和 service
进行组合:
package web
var PostSet = wire.NewSet(NewPostHandler, service.NewPostService)
使用 wire.NewSet
函数将提供者进行分组,该函数返回一个 ProviderSet
结构体。不仅如此,wire.NewSet
还能对多个 ProviderSet
进行分组 wire.NewSet(PostSet, XxxSet)
。
对于之前的 InitializeApp
函数,我们可以这样升级:
func InitializeAppV2() *gin.Engine {
wire.Build(
web.PostSet,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
然后通过 Wire
命令生成代码,和之前的结果一致。
Wire 注入器(injectors)
注入器(injectors
)的作用是将所有的提供者(providers
)连接起来,回顾一下我们之前的代码:
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
)
return &gin.Engine{}
}
InitializeApp
函数就是一个注入器,函数内部通过 wire.Build
函数连接所有的提供者,然后返回 &gin.Engine{}
,该返回值实际上并没有使用到,只是为了满足编译器的要求,避免报错而已,真正的返回值来自 ioc.NewGinEngineAndRegisterRoute
。
Wire 高级应用
绑定接口
回顾我们之前编写的代码:
package web
···
func NewPostHandler(serv service.IPostService) *PostHandler {
return &PostHandler{serv: serv}
}
···
pakacge service
···
func NewPostService() IPostService {
return &PostService{}
}
···
NewPostHandler
函数依赖于 service.IPostService
接口,NewPostService
函数返回的是 IPostService
接口的值,这两个地方的类型匹配,因此 Wire
工具能够正确识别并生成代码。然而,这并不是推荐的最佳实践。因为在 Go
中的 最佳实践 是返回 具体的类型 的值,所以最好让 NewPostService
返回具体类型 PostService
的值:
func NewPostService() *PostService {
return &PostService{}
}
但是这样,Wire
工具将认为 IPostService
接口类型与 PostService
类型不匹配,导致生成代码失败。因此我们需要修改注入器的代码:
func InitializeApp() *gin.Engine {
wire.Build(
web.NewPostHandler,
service.NewPostService,
ioc.NewGinEngineAndRegisterRoute,
wire.Bind(new(service.IPostService), new(*service.PostService)),
)
return &gin.Engine{}
}
使用 wire.Bind
来建立接口类型和具体的实现类型之间的绑定关系,这样 Wire
工具就可以根据这个绑定关系进行类型匹配并生成代码。
wire.Bind
函数的第一个参数是指向所需接口类型值的指针,第二个实参是指向实现该接口的类型值的指针。
结构体提供者(Struct Providers)
Wire
库有一个函数是 wire.Struct
,它能根据现有的类型进行构造结构体,我们来看看下面的例子:
package main
type Name string
func NewName() Name {
return"Jack"
}
type PublicAccount string
func NewPublicAccount() PublicAccount {
return"Hello World"
}
type User struct {
MyName Name
MyPublicAccount PublicAccount
}
func InitializeUser() *User {
wire.Build(
NewName,
NewPublicAccount,
wire.Struct(new(User), "MyName", "MyPublicAccount"),
)
return &User{}
}
上述代码中,首先定义了自定义类型 Name
和 PublicAccount
以及结构体类型 User
,并分别提供了 Name
和 PublicAccount
的初始化函数(providers
)。然后定义一个注入器(injectors
)InitializeUser
,用于构造连接提供者并构造 *User
实例。
使用 wire.Struct
函数需要传递两个参数,第一个参数是结构体类型的指针值,另一个参数是一个可变参数,表示需要注入的结构体字段的名称集。
根据上述代码,使用 Wire
工具生成的代码如下所示:
func InitializeUser() *User {
name := NewName()
publicAccount := NewPublicAccount()
user := &User{
MyName: name,
MyPublicAccount: publicAccount,
}
return user
}
如果我们不想返回指针类型,只需要修改 InitializeUser
函数的返回值为非指针即可。
绑定值
有时候,我们可以在注入器中通过 值表达式 给一个类型进行赋值,而不是依赖提供者(providers
)。
func InjectUser() User {
wire.Build(wire.Value(User{MyName: "Jack"}))
return User{}
}
在上述代码中,使用 wire.Value
函数通过表达式直接指定 MyName
的值,生成的代码如下所示:
func InjectUser() User {
user := _wireUserValue
return user
}
var (
_wireUserValue = User{MyName: "Jack"}
)
需要注意的是,值表达式将被复制到生成的代码文件中。
对于接口类型,可以使用 InterfaceValue
:
func InjectPostService() service.IPostService {
wire.Build(wire.InterfaceValue(new(service.IPostService), &service.PostService{}))
return nil
}
使用结构体字段作为提供者(providers)
有些时候,你可以使用结构体的某个字段作为提供者,从而生成一个类似 GetXXX
的函数。
func GetUserName() Name {
wire.Build(
NewUser,
wire.FieldsOf(new(User), "MyName"),
)
return ""
}
你可以使用 wire.FieldsOf
函数添加任意字段,生成的代码如下所示:
func GetUserName() Name {
user := NewUser()
name := user.MyName
return name
}
func NewUser() User {
return User{MyName: Name("Jack"), MyPublicAccount: PublicAccount("HelloWorld")}
}
清理函数
如果一个提供者创建了一个需要清理的值(例如关闭一个文件),那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数,或者在注入器实现中稍后调用的提供商返回错误时清理资源。
并且 Wire
对 Provider
的返回值个数及顺序有以下限制:
- 第一个返回值是需要生成的对象
- 如果有 2 个返回值,第二个返回值必须是 func() 或 error
- 如果有 3 个返回值,第二个返回值必须是 func(),而第三个返回值必须是
// db.go
func InitGormDB()(*gorm.DB, func(), error) {
// 初始化db链接
// ...
cleanFunc := func(){
db.Close()
}
return db, cleanFunc, nil
}
// wire.go
func BuildInjector() (*Injector, func(), error) {
wire.Build(
common.InitGormDB,
// ...
NewInjector
)
return new(Injector), nil, nil
}
// 生成的wire_gen.go
func BuildInjector() (*Injector, func(), error) {
db, cleanup, err := common.InitGormDB()
// ...
return injector, func(){
// 所有provider的清理函数都会在这里
cleanup()
}, nil
}
// main.go
injector, cleanFunc, err := app.BuildInjector()
defer cleanFunc()
备用注入器语法
如果你不喜欢将类似这种写法 → return &gin.Engine{}
放在你的注入器函数声明的末尾,你可以用 panic
来更简洁地写它:
func InitializeGin() *gin.Engine {
panic(wire.Build(/* ... */))
}
总结
在本文中,我们详细探讨了 Go Wire
工具的基本用法和高级特性。它是一个专为依赖注入设计的代码生成工具,它不仅提供了基础的依赖解析和代码生成功能,还支持多种高级用法,如接口绑定和构造结构体。
依赖注入的设计模式应用非常广泛,Wire
工具让依赖注入在 Go
语言中变得更简单。
本文的所有代码在这里。
标签:Wire,return,service,wire,func,使用,go From: https://www.cnblogs.com/gui-lin/p/18066085