dedecms 做网站,提升学历的四种方式,校园门户网站系统建设,网站建设骗子公司文章目录 一、引入二、控制反转与依赖注入三、为什么需要依赖注入工具3.1 示例3.2 依赖注入写法与非依赖注入写法 四、wire 工具介绍与安装4.1 wire 基本介绍4.2 安装 五、Wire 的基本使用5.1 前置代码准备5.2 使用 Wire 工具生成代码 六、Wire 核心技术5.1 抽象语法树分析5.2 … 文章目录 一、引入二、控制反转与依赖注入三、为什么需要依赖注入工具3.1 示例3.2 依赖注入写法与非依赖注入写法 四、wire 工具介绍与安装4.1 wire 基本介绍4.2 安装 五、Wire 的基本使用5.1 前置代码准备5.2 使用 Wire 工具生成代码 六、Wire 核心技术5.1 抽象语法树分析5.2 模板编程 七、Wire 的核心概念7.1 两个核心概念7.2 Wire 提供者providers7.3 Wire 注入器injectors 八、Wire 的高级用法8.1 绑定接口8.2 结构体提供者Struct Providers8.3 绑定值8.4 使用结构体字段作为提供者providers8.5 清理函数8.6 备用注入器语法 九、参考文档 一、引入
在Go语言的项目开发中为了提高代码的可测试性和可维护性我们通常会采用依赖注入Dependency Injection简称DI的设计模式。依赖注入可以让高层模块不依赖底层模块的具体实现而是通过抽象来互相依赖从而使得模块之间的耦合度降低系统的灵活性和可扩展性增强。
二、控制反转与依赖注入
控制反转Inversion of Control缩写为IoC是面向对象编程中的一种设计原则可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入Dependency Injection简称DI。依赖注入是生成灵活和松散耦合代码的标准技术通过明确地向组件提供它们所需要的所有依赖关系。在 Go 中通常采用将依赖项作为参数传递给构造函数的形式
构造函数NewUserRepository在创建UserRepository时需要从外部将依赖项db作为参数传入我们在UserRepository中无需关注db的创建逻辑实现了代码解耦。
// NewUserRepository 创建BookRepo的构造函数
func NewUserRepository(db *gorm.DB) *UserRepository {return UserRepository{db: db}
}
区别于控制反转如果在NewUserRepository函数中自行创建相关依赖这将导致代码高度耦合并且难以维护和调试。
// NewUserRepository 创建UserRepository的构造函数
func NewUserRepository() *UserRepository {db, _ : gorm.Open(sqlite.Open(gorm.db), gorm.Config{})return UserRepository{db: db}
}三、为什么需要依赖注入工具
3.1 示例
如果上面示例代码不够清晰的话我们来看这两段代码
// NewUserRepositoryV1非依赖注入的写法
func NewUserRepositoryV1(dbCfg DBConfig, c CacheConfig)*UserRepository{db, err : gorm.Open(mysql.Open(dbcfg.DSN))if err ! nil {panic(err)}ud dao.NewUserDAO(db)uc cache.NewUserCache(redis.NewClient(redis.Options{Addr: c.Addr,}))return UserRepository{dao: ud,cache: uc,}
}// NewUserRepository 依赖注入的写法
func NewUserRepository(d *dao.UserDAO, c *cache.UserCache)*UserRepository{return UserRepository{dao: d,cache: c,}
}
可以清楚地看到这两段代码展示了在Go语言中实现依赖注入的两种不同方式。 第一段代码 NewUserRepositoryV1 是非依赖注入的写法。在这个函数中UserRepository 的依赖db 和 cache是在函数内部创建的。这种方式的问题在于它违反了单一职责原则因为 NewUserRepositoryV1 不仅负责创建 UserRepository 实例还负责创建其依赖的数据库和缓存客户端。这样做会导致代码耦合度较高难以测试和维护。 第二段代码 NewUserRepository 是依赖注入的写法。这个函数接受 UserRepository 的依赖*dao.UserDAO 和 *cache.UserCache作为参数而不是在函数内部创建它们。这种方式使得 UserRepository 的创建与它的依赖解耦更容易测试因为你可以轻松地为 UserRepository 提供模拟的依赖项。此外这种写法也更符合依赖注入的原则因为它将控制反转给了调用者由调用者来决定 UserRepository 实例化时使用哪些依赖项。
3.2 依赖注入写法与非依赖注入写法
依赖注入写法不关心依赖是如何构造的。
非依赖注入写法必须自己初始化依赖比如说 Repository 需要知道如何初始化 DAO 和 Cache。由此带来的缺点是
深度耦合依赖的初始化过程。往往需要定义额外的 Config 类型来传递依赖所需的配置信息。一旦依赖增加新的配置或者更改了初始化过程都要跟着修改。缺乏扩展性。测试不友好。难以复用公共组件例如 DB 或 Redis 之类的客户端。
四、wire 工具介绍与安装
4.1 wire 基本介绍 Wire 是一个的 Google 开源专为依赖注入Dependency Injection设计的代码生成工具通过自动生成代码的方式在初始编译过程中完成依赖注入。它可以自动生成用于化各种依赖关系的代码从而帮助我们更轻松地管理和注入依赖关系。 Wire 分成两部分一个是在项目中使用的依赖 一个是命令行工具。
4.2 安装
go install github.com/google/wire/cmd/wirelatest五、Wire 的基本使用
5.1 前置代码准备
目录结构如下
wire
├── db.go # 数据库相关代码
├── go.mod # Go模块依赖配置文件
├── go.sum # Go模块依赖校验文件
├── main.go # 程序入口文件
├── repository # 存放数据访问层代码的目录
│ ├── dao # 数据访问对象DAO目录
│ │ └── user.go # 用户相关的DAO实现
│ └── user.go # 用户仓库实现
├── wire.go # Wire依赖注入配置文件repository/dao/user.go文件
// repository/dao/user.go
package daoimport gorm.io/gormtype UserDAO struct {db *gorm.DB
}func NewUserDAO(db *gorm.DB) *UserDAO {return UserDAO{db: db,}
}repository/user.go 文件
// repository/user.go
package repositoryimport wire/repository/daotype UserRepository struct {dao *dao.UserDAO
}func NewUserRepository(dao *dao.UserDAO) *UserRepository {return UserRepository{dao: dao,}
}db.go 文件:
// db.go
package wireimport (gorm.io/driver/mysqlgorm.io/gorm
)func InitDB() *gorm.DB {db, err : gorm.Open(mysql.Open(dsn))if err ! nil {panic(err)}return db
}main.go 文件:
package wireimport (fmtgorm.io/driver/mysqlgorm.io/gormwire/repositorywire/repository/dao
)func main() {// 非依赖注入db, err : gorm.Open(mysql.Open(dsn))if err ! nil {panic(err)}ud : dao.NewUserDAO(db)repo : repository.NewUserRepository(ud)fmt.Println(repo)
}5.2 使用 Wire 工具生成代码
现在我们已经有了基本的代码结构接下来我们将使用 wire 工具来生成依赖注入的代码。
首先确保你已经安装了 wire 工具。如果没有安装可以使用以下命令安装
go get github.com/google/wire/cmd/wire接下来我们需要创建一个 wire 的配置文件通常命名为 wire.go。在这个文件中我们将使用 wire 的语法来指定如何构建 UserRepository 实例。
wire.go 文件:
//go:build wireinject// 让 wire 来注入这里的代码
package wireimport (github.com/google/wirewire/repositorywire/repository/dao
)func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西但是具体怎么构造怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository)
}
这段代码是使用 wire 工具进行依赖注入的配置文件。在这个文件中我们定义了一个函数 InitRepository这个函数的目的是为了生成一个 *repository.UserRepository 的实例。但是这个函数本身并不包含具体的实现代码而是依赖于 wire 工具来注入依赖。 让我们逐步解释这段代码 构建约束指令: //go:build wireinject这行注释是一个构建约束它告诉 go build 只有在满足条件 wireinject 的情况下才应该构建这个文件。wireinject 是一个特殊的标签用于指示 wire 工具处理这个文件。 导入包: import (github.com/google/wirewire/repositorywire/repository/dao
)这部分导入了必要的包包括 wire 工具库以及项目中的 repository 和 dao 包这些包包含了我们需要注入的依赖。 InitRepository 函数: func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西但是具体怎么构造怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository)
}这个函数是 wire 注入的目标。它声明了一个返回 *repository.UserRepository 的函数但是函数体内部没有具体的实现代码。wire.Build 函数调用是关键 主要是连接或绑定我们之前定义的所有初始化函数。当我们运行 wire 工具来生成代码时它就会根据这些依赖关系来自动创建和注入所需的实例。这些函数按照依赖关系被调用以正确地构造和注入 UserRepository 实例所需的依赖。 InitDB 是初始化数据库连接的函数。repository.NewUserRepository 是创建 UserRepository 实例的函数。dao.NewUserDAO 是创建 UserDAO 实例的函数。 wire 工具会自动生成这些函数调用的代码并确保依赖关系得到满足。 返回语句: return new(repository.UserRepository)这个返回语句是必须的尽管它实际上并不会被执行。wire 工具会生成一个替换这个函数体的代码其中包括所有必要的依赖注入逻辑。 在编写完 wire.go 文件后你需要运行 wire 命令来生成实际的依赖注入代码。生成的代码将被放在一个名为 wire_gen.go 的文件中这个文件应该被提交到你的版本控制系统中。
现在我们可以运行 wire 命令来生成依赖注入的代码
wire这个命令会扫描 wire.go 文件并生成一个新的 Go 文件 wire_gen.go其中包含了 InitializeUserRepository 函数的实现这个函数会创建并返回一个 UserRepository 实例其依赖项已经自动注入。 生成 wire_gen.go 文件内容如下所示
// Code generated by Wire. DO NOT EDIT.//go:generate go run -modmod github.com/google/wire/cmd/wire
//go:build !wireinject
// build !wireinjectpackage wireimport (wire/repositorywire/repository/dao
)// Injectors from wire.go:func InitRepository() *repository.UserRepository {db : InitDB()userDAO : dao.NewUserDAO(db)userRepository : repository.NewUserRepository(userDAO)return userRepository
}最后我们需要修改 main.go 文件使用 wire 生成的代码来获取 UserRepository 实例
package wirefunc main() {InitRepository()
}现在当我们运行 main.go 时它将使用 wire 工具生成的代码来初始化 UserRepository包括其依赖的 UserDAO 和数据库连接。这样我们就实现了依赖注入并且代码更加简洁、易于维护。
六、Wire 核心技术
5.1 抽象语法树分析
wire 工具的工作原理是基于对Go代码的抽象语法树Abstract Syntax Tree简称AST的分析。AST是源代码的抽象语法结构的树状表示它以树的形式表现编程语言的语法结构。wire 工具通过分析AST来理解代码中的依赖关系。 在Go中go/ast 包提供了解析Go源文件并构建AST的功能。wire 工具利用这个包来遍历和分析项目的Go代码识别出所有的依赖项并构建出依赖关系图。这个依赖关系图随后被用来生成注入依赖的代码。
5.2 模板编程
wire 工具生成代码的过程也涉及到模板编程。模板编程是一种编程范式它允许开发者定义一个模板然后使用具体的数据来填充这个模板生成最终的代码或文本。 在wire中虽然不直接使用Go语言的模板引擎如text/template或html/template但它的工作原理与模板编程类似。wire定义了一套自己的语法来描述依赖关系然后根据这些描述生成具体的Go代码。 wire的语法主要包括以下几个部分
wire.NewSet定义一组相关的依赖通常包括一个或多个构造函数。wire.Build指定生成代码时应该使用哪些依赖集合。bind 函数用于绑定接口和实现告诉wire如何创建接口的实例。 wire工具通过这些语法来构建一个依赖图然后根据这个图生成一个函数该函数负责创建并返回所有必要的组件实例同时处理它们之间的依赖关系。 通过结合抽象语法树分析和模板编程wire 工具能够提供一种声明式的依赖注入方法让开发者能够专注于定义依赖关系而不是手动编写依赖注入的代码。这不仅减少了重复劳动还提高了代码的可维护性和降低了出错的可能性。
七、Wire 的核心概念
7.1 两个核心概念
在 wire 中有两个核心概念提供者providers和注入器injectors。
7.2 Wire 提供者providers
提供者 是一个普通有返回值的 Go 函数它负责创建一个对象或者提供依赖。在 wire 的上下文中提供者可以是任何返回一个或多个值的函数。这些返回值将成为注入器函数的参数。提供者函数通常负责初始化组件比如数据库连接、服务实例等。并且提供者的返回值不仅限于一个如果有需要的话可以额外添加一个 error 的返回值。 例如一个提供者函数可能会创建并返回一个数据库连接
func NewDBConnection(dsn string) (*gorm.DB, error) {db, err : gorm.Open(mysql.Open(dsn))if err ! nil {return nil, err}return db, nil
}提供者函数可以分组为提供者函数集provider set。使用wire.NewSet 函数可以将多个提供者函数添加到一个集合中。举个例子例如将 user 相关的 handler 和 service 进行组合
package webvar UserSet wire.NewSet(NewUserHandler, service.NewUserService)使用 wire.NewSet 函数将提供者进行分组该函数返回一个 ProviderSet 结构体。不仅如此wire.NewSet 还能对多个 ProviderSet 进行分组 wire.NewSet(UserSet, XxxSet) 。
package demoimport (// ...example.com/some/other/pkg
)// ...var MegaSet wire.NewSet(UserSet, pkg.OtherSet)7.3 Wire 注入器injectors
注入器injectors的作用是将所有的提供者providers连接起来要声明一个注入器函数只需要在函数体中调用wire.Build()。这个函数的返回值也无关紧要只要它们的类型正确即可。这些值在生成的代码中将被忽略。回顾一下我们之前的代码
//go:build wireinject// 让 wire 来注入这里的代码
package wireimport (github.com/google/wirewire/repositorywire/repository/dao
)func InitRepository() *repository.UserRepository {// 我只在这里声明我要用的各种东西但是具体怎么构造怎么编排顺序// 这个方法里面传入各个组件的初始化方法wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return new(repository.UserRepository)
}在这个例子中InitRepository 是一个注入器它依赖 InitDB 和 repository.NewUserRepository 这两个提供者。
与提供者一样注入器也可以输入参数然后将其发送给提供者并且可以返回错误。wire.Build的参数和wire.NewSet一样都是提供者集合。这些就在该注入器的代码生成期间使用的提供者集。
八、Wire 的高级用法
8.1 绑定接口
依赖项注入通常用于绑定接口的具体实现。wire通过类型标识将输入与输出匹配因此倾向于创建一个返回接口类型的提供者。然而这也不是习惯写法因为Go的最佳实践是返回具体类型。你可以在提供者集中声明接口绑定.
我们对之前的代码进行改造:
首先我们在UserRepository接口中定义一些方法。例如我们可以定义一个GetUser方法该方法接收一个用户ID并返回相应的用户。 在repository/user.go文件中
package repositoryimport (wire/repository/daogorm.io/gorm
)type UserRepository interface {GetUser(id uint) (*User, error)
}type UserRepositoryImpl struct {dao *dao.UserDAO
}func (r *UserRepositoryImpl) GetUser(id uint) (*User, error) {return r.dao.GetUser(id)
}func NewUserRepository(dao *dao.UserDAO) UserRepository {return UserRepositoryImpl{dao: dao,}
}然后我们在UserDAO中实现这个GetUser方法。在repository/dao/user.go文件中
package daoimport (gorm.io/gorm
)type User struct {ID uint// other fields...
}type UserDAO struct {db *gorm.DB
}func (dao *UserDAO) GetUser(id uint) (*User, error) {var user Userresult : dao.db.First(user, id)if result.Error ! nil {return nil, result.Error}return user, nil
}func NewUserDAO(db *gorm.DB) *UserDAO {return UserDAO{db: db,}
}最后我们需要更新wire.go文件中的InitRepository函数以返回UserRepository接口而不是具体的实现。 在wire.go文件中
//go:build wireinjectpackage wireimport (github.com/google/wirewire/repositorywire/repository/dao
)func InitRepository() repository.UserRepository {wire.Build(InitDB, repository.NewUserRepository, dao.NewUserDAO)return repository.UserRepositoryImpl{}
}使用 wire.Bind 来建立接口类型和具体的实现类型之间的绑定关系这样 Wire 工具就可以根据这个绑定关系进行类型匹配并生成代码。
wire.Bind 函数的第一个参数是指向所需接口类型值的指针第二个实参是指向实现该接口的类型值的指针。
8.2 结构体提供者Struct Providers
Wire 库有一个函数是 wire.Struct它能根据现有的类型进行构造结构体我们来看看下面的例子
package mainimport github.com/google/wiretype Name stringfunc NewName() Name {return 小米SU7
}type PublicAccount stringfunc NewPublicAccount() PublicAccount {return 新一代车神
}type User struct {MyName NameMyPublicAccount PublicAccount
}func InitializeUser() *User {wire.Build(NewName,NewPublicAccount,wire.Struct(new(User), MyName, MyPublicAccount),)return User{}
}上述代码中首先定义了自定义类型 Name 和 PublicAccount 以及结构体类型 User并分别提供了 Name 和 PublicAccount 的初始化函数providers。然后定义一个注入器injectorsInitializeUser用于构造连接提供者并构造 *User 实例。
使用 wire.Struct 函数需要传递两个参数第一个参数是结构体类型的指针值另一个参数是一个可变参数表示需要注入的结构体字段的名称集。
根据上述代码使用 Wire 工具生成的代码如下所示
func InitializeUser() *User {name : NewName()publicAccount : NewPublicAccount()user : User{MyName: name,MyPublicAccount: publicAccount,}return user
}如果我们不想返回指针类型只需要修改 InitializeUser 函数的返回值为非指针即可。
8.3 绑定值
有时将基本值通常为nil绑定到类型是有用的。你可以向提供程序集添加一个值表达式而不是让注入器依赖于一次性函数提供者providers。
func InjectUser() User {wire.Build(wire.Value(User{MyName: 小米SU7}))return User{}
}在上述代码中使用 wire.Value 函数通过表达式直接指定 MyName 的值生成的代码如下所示
func InjectUser() User {user : _wireUserValuereturn user
}var (_wireUserValue User{MyName: 小米SU7}
)需要注意的是值表达式将被复制到生成的代码文件中。
对于接口类型可以使用 InterfaceValue
func InjectPostService() service.IPostService {wire.Build(wire.InterfaceValue(new(service.IPostService), service.PostService{}))return nil
}8.4 使用结构体字段作为提供者providers
有些时候你可以使用结构体的某个字段作为提供者从而生成一个类似 GetXXX 的函数。
func GetUserName() Name {wire.Build(NewUser,wire.FieldsOf(new(User), MyName),)return
}你可以使用 wire.FieldsOf 函数添加任意字段生成的代码如下所示
func GetUserName() Name {user : NewUser()name : user.MyNamereturn name
}func NewUser() User {return User{MyName: Name(小米SU7), MyPublicAccount: PublicAccount(新一代车神!)}
}8.5 清理函数
如果一个提供者创建了一个需要清理的值例如关闭一个文件那么它可以返回一个闭包来清理资源。注入器会用它来给调用者返回一个聚合的清理函数或者在注入器实现中稍后调用的提供商返回错误时清理资源。
func provideFile(log Logger, path Path) (*os.File, func(), error) {f, err : os.Open(string(path))if err ! nil {return nil, nil, err}cleanup : func() {if err : f.Close(); err ! nil {log.Log(err)}}return f, cleanup, nil
}8.6 备用注入器语法
如果你不喜欢在注入器函数声明的末尾编写类似return Foo{}, nil的语句那么你可以简单粗暴地使用panic
func InitializeGin() *gin.Engine {panic(wire.Build(/* ... */))
}九、参考文档
掘金依赖注入工具-wire李文周的博客-依赖注入工具-wire