控制反转和依赖注入
控制反转(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 语言中变得更简单。
本文的所有代码在这里。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |