目录
一、引入
在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/wire@latest
复制代码 五、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 dao
- import "gorm.io/gorm"
- type UserDAO struct {
- db *gorm.DB
- }
- func NewUserDAO(db *gorm.DB) *UserDAO {
- return &UserDAO{
- db: db,
- }
- }
复制代码 repository/user.go 文件:- // repository/user.go
- package repository
- import "wire/repository/dao"
- type UserRepository struct {
- dao *dao.UserDAO
- }
- func NewUserRepository(dao *dao.UserDAO) *UserRepository {
- return &UserRepository{
- dao: dao,
- }
- }
复制代码 db.go 文件:- // db.go
- package wire
- import (
- "gorm.io/driver/mysql"
- "gorm.io/gorm"
- )
- func InitDB() *gorm.DB {
- db, err := gorm.Open(mysql.Open("dsn"))
- if err != nil {
- panic(err)
- }
- return db
- }
复制代码 main.go 文件:- package wire
- import (
- "fmt"
- "gorm.io/driver/mysql"
- "gorm.io/gorm"
- "wire/repository"
- "wire/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 wire
- import (
- "github.com/google/wire"
- "wire/repository"
- "wire/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 的情况下才应该构建这个文件。wireinject 是一个特殊的标签,用于指示 wire 工具处理这个文件。
- 导入包:
- import (
- "github.com/google/wire"
- "wire/repository"
- "wire/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.go 文件,并生成一个新的 Go 文件 wire_gen.go,其中包含了 InitializeUserRepository 函数的实现,这个函数会创建并返回一个 UserRepository 实例,其依赖项已经自动注入。
生成 wire_gen.go 文件,内容如下所示:- // 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 (
- "wire/repository"
- "wire/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 wire
- func 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 web
- var UserSet = wire.NewSet(NewUserHandler, service.NewUserService)
复制代码 使用 wire.NewSet 函数将提供者进行分组,该函数返回一个 ProviderSet 结构体。不但云云,wire.NewSet 还能对多个 ProviderSet 进行分组 wire.NewSet(UserSet, XxxSet) 。- package demo
- import (
- // ...
- "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 wire
- import (
- "github.com/google/wire"
- "wire/repository"
- "wire/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 repository
- import (
- "wire/repository/dao"
- "gorm.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 dao
- import (
- "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 User
- result := 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/wire"
- "wire/repository"
- "wire/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 main
- import "github.com/google/wire"
- type Name string
- func NewName() Name {
- return "小米SU7"
- }
- type PublicAccount string
- func NewPublicAccount() PublicAccount {
- return "新一代车神"
- }
- 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 函数的返回值为非指针即可。
8.3 绑定值
偶然,将基本值(通常为nil)绑定到类型是有用的。你可以向提供程序集添加一个值表达式,而不是让注入器依赖于一次性函数提供者(providers)。- func InjectUser() User {
- wire.Build(wire.Value(User{MyName: "小米SU7"}))
- return User{}
- }
复制代码 在上述代码中,使用 wire.Value 函数通过表达式直接指定 MyName 的值,生成的代码如下所示:- func InjectUser() User {
- user := _wireUserValue
- return 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.MyName
- return 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(/* ... */))
- }
复制代码 九、参考文档
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |