Docs Vault

在开发过程中,随着开发者对功能需求的理解越来越深入,对代码结构越来越熟悉,并且在开发过程中,持续不断的思考代码的实现方式,在项目开发到一定阶段后,开发者很可能会涌现出更好的设计思路和实现方法。因此,在项目开发过程中,或者在项目开发到某个阶段后,可以回过头来优化、完善代码,甚至升级项目的实现方式。


同样,在 miniblog 项目的开发过程中,也不断涌现出更加优化的方案。本节课及下节课将结合开发中的新思路和实现方法,对项目进行优化改进,并对软件架构进行升级。


提示:
本节课及下一节课最终源码位于 miniblog 项目的 feature/s35 分支。


本节课先来介绍下如何使用 wire 实现自动依赖注入。


Wire 依赖注入实现


在 Go 项目开发过程中,经常需要创建各种实例,这些实例可能依赖于其他实例。通常,我们首先需要创建依赖实例,然后将它们作为参数传递给其他实例的构造函数。在项目规模较小时,这种做法并不会带来太大问题。


然而,随着项目规模的逐渐扩大,依赖实例的数量和依赖关系也随之增加。这时,开发者需要按照依赖关系的顺序创建各实例,并将它们传递给其他实例的构造函数。如果依赖关系发生变化,还需要修改构造函数的参数列表。这种管理方式不仅会降低开发效率,还可能引入一些难以发现的错误。依赖注入的出现解决了这个问题。通过将对象之间的依赖关系从代码中抽离出来,可以使代码更加灵活和可维护。同时,依赖注入还可以使代码更加可测试,因为可以使用不同的依赖来测试对象的不同行为。


在 Go 项目开发中可以使用 github.com/google/wire 包来实现依赖注入。


什么是依赖注入


依赖注入(Dependency Injection,DI)是一种设计模式,用于管理对象之间的依赖关系。依赖注入的核心思想是将对象的依赖关系从代码中分离出来,从而使代码更加灵活和可维护。在依赖注入中,对象不再负责创建它所依赖的对象,而是由外部容器来负责创建和管理对象之间的依赖关系。


在 Go 中,依赖注入有以下三种常见的实现方式:

  1. 构造函数注入:通过在对象初始化时,将所需的依赖作为参数传递给构造函数。这种方式最为常见,能够确保对象在创建时依赖关系被完整注入并初始化;
  2. 属性注入(字段注入):通过将依赖声明为结构体的公开字段或属性,并在实例化后通过外部赋值方式注入依赖。这种方式将依赖注入逻辑与对象的创建逻辑解耦,但可能增加维护成本;
  3. 方法注入:将依赖作为参数传递给对象的方法,而不是在对象生命周期中长期持有依赖。这种方式适用于依赖只在特定方法中的短期使用场景。


Wire 及核心概念介绍



wire 是由谷歌开源的一个供 Go 语言使用的依赖注入工具。它能够根据代码,生成相应的依赖注入 Go 代码。Wire 通过 wire 命令来自动生成 injector 函数。wire 命令可以通过以下命令安装:

$ go install github.com/google/wire/cmd/wire@latest

wire 中最核心的三个概念就是 Provider、ProviderSet 和 Injector。


(1)Provider


Provider 是 Wire 用于创建某种类型实例的构造函数,本质上是为依赖注入提供实际对象的工厂方法或生成器。它是一个返回指定类型实例的函数。如果该函数本身的参数存在依赖,Wire 会根据依赖关系自动解析并注入这些参数。Provider 函数返回的实例将被加入到依赖图中,用于满足其他依赖的需求。


例如,以下是一个简单的 Provider 函数:

package main  

import "github.com/google/wire"  

// 提供一个配置结构体  
type Config struct {  
    Name string  
}  

// 提供构造函数,返回 Config 实例  
func NewConfig() *Config {  
    return &Config{  
        Name: "MyApp",  
    }  
}  

// 提供一个服务  
type Service struct {  
    Config *Config  
}  

// 提供构造函数,根据 Config 创建 Service  
func NewService(config *Config) *Service {  
    return &Service{  
        Config: config,  
    }  
}  

// 声明 ProviderSet:将所有构造函数注册到 Wire 的依赖图  
var ProviderSet = wire.NewSet(NewConfig, NewService)

NewConfig 和 NewService 是 Provider,它们返回程序需要的类型(*Config 和 *Service)。Wire 会将这些构造函数加入依赖图中,并根据它们声明的依赖关系自动生成注入代码。


(2)ProviderSet


ProviderSet 是 Provider 的集合,用于声明应用程序中所有的依赖是如何创建的。一个典型的 ProviderSet 如下:

// ProviderSet 是用于依赖注入的 Provider 集合,提供 DB 实例和 UserRetriever 实例.
// Wire 会根据此集合生成初始化代码.
var ProviderSet = wire.NewSet(
    ProvideDB,            // 提供数据库实例
    ProvideUserRetriever, // 提供用户检索服务
    // 将 UserRetriever 实现绑定到接口 mw.UserRetriever
    wire.Bind(new(mw.UserRetriever), new(*UserRetriever)),
    // 将 Config 的 MySQLOptions 字段注入到依赖树中
    wire.FieldsOf(new(Config), "MySQLOptions"),
)

这里的 ProviderSet 包含了多个 wire 的 Provider,用于描述整个依赖关系。


(3)Injector


Injector 是依赖注入的入口函数,负责初始化最终对象及其所有依赖,并返回完整的实例。它由 Wire 根据 wire.Build 的声明自动生成,通过调用相应的 Provider,按照依赖关系完成实例化和注入。开发者只需调用 Injector 函数,即可获取所需对象,无需手动管理复杂的依赖关系。


例如,一个 Injector 函数如下:

// wire.go  
// +build wireinject  

package main  

import "github.com/google/wire"  

// Wire 的声明入口:告诉 Wire 如何组织依赖  
func InitializeService() (*Service, error) {  
    panic(wire.Build(ProviderSet))  
}

运行 wire . 命令生成 Injector 代码。生成的 Injector 代码会保存在 wire_gen.go 文件中,内容如下:

// wire_gen.go  
// Code generated by Wire. DO NOT EDIT.  

func InitializeService() (*Service, error) {  
    config := NewConfig()           // 调用 NewConfig() 构造 *Config  
    service := NewService(config)  // 调用 NewService(*Config) 构造 *Service  
    return service, nil  
}


Wire 基本用法


wire 的使用方式,见 miniblog 项目仓库 feature/s32 分支中的 docs/book/wire.md 文件。


miniblog 依赖注入实现


在 miniblog 项目中,使用依赖注入创建了 server.Server 类型的实例。server.Server 接口定义如下:

// Server 定义所有服务器类型的接口.
type Server interface {
    // RunOrDie 运行服务器,如果运行失败会退出程序(OrDie的含义所在).
    RunOrDie()
    // GracefulStop 方法用来优雅关停服务器。关停服务器时需要处理 context 的超时时间.
    GracefulStop(ctx context.Context)
}


(1)定义 Provider


创建 server.Server 类型的实例依赖了很多 Provider,这些 Provider 定义在不同的 Go 源文件中。你可以在 internal/apiserver/wire.go 文件中,查看到使用到的 Provider。


(2)创建 Injector


为了便于维护和查找,Injector 的函数签名保存在一个单独的、约定俗称的文件 internal/apiserver/wire.go 中,代码如下:

func InitializeWebServer(*Config) (server.Server, error) {
    wire.Build(
        wire.NewSet(NewWebServer, wire.FieldsOf(new(*Config), "ServerMode")),
        wire.Struct(new(ServerConfig), "*"), // * 表示注入全部字段
        wire.NewSet(store.ProviderSet, biz.ProviderSet),
        ProvideDB, // 提供数据库实例
        validation.ProviderSet,
        wire.NewSet(
            wire.Struct(new(UserRetriever), "*"),
            wire.Bind(new(ginmw.UserRetriever), new(*UserRetriever)),
        ),
        auth.ProviderSet,
    )
    return nil, nil
}

上述代码定义了一个用 Google Wire 实现的依赖注入逻辑,为 InitializeWebServer 函数生成依赖关系图,自动注入需要的组件,最终构建出一个完整的 server.Server 实例。函数 InitializeWebServer 接收一个 *Config 对象作为输入。*Config 中,包含了创建 server.Server 类型实例的所有依赖项。wire.Build 告诉 Wire 如何按照依赖关系注入和构建 server.Server 对象。


(3)使用 wire 命令生成依赖函数


在 internal/apiserver/ 目录下执行以下命令,来生成依赖函数:

$ wire .

上述命令会在当前命令下生成 wire_gen.go 文件,内容见 internal/apiserver/wire_gen.go,其中的依赖有顺序,且复杂,但 Wire 框架替我们管理了这些依赖。生成的 wire_go.go 文件中,被自动注入了以下代码行:

//go:generate go run -mod=mod github.com/google/wire/cmd/wire

上述代码意味着,如果我们的依赖有更新,只需要更新 wire.go 文件中的 InitializeServerConfig 函数签名,然后执行:go generate ./... 命令即可。


这里需要注意,如果你正处在 Go Workspace 模式下,需要修改上述的 -mod=mod 为 -mod=readonly。


提示:
运行 go env GOWORK 命令,如果输出一个路径(例如 /path/to/your/go.work),说明你正在使用 workspace 模式。如果输出为空,说明 workspace 模式未启用。


在 Go Workspace 模式下,-mod 标志只能设置为 readonly 或者 vendor,否则在执行 go generate 命令时会报以下错误:

go: -mod may only be set to readonly or vendor when in workspace mode, but it is set to "mod"
Remove the -mod flag to use the default readonly value,
or set GOWORK=off to disable workspace mode.


(4)使用生成的依赖函数


修改 internal/apiserver/server.go 文件,将之前创建 server.Server 类型实例的方式,改成以下方式:

// NewUnionServer 根据配置创建联合服务器.
func (cfg *Config) NewUnionServer() (*UnionServer, error) {
    ...
    log.Infow("Initializing federation server", "server-mode", cfg.ServerMode)

    // 创建服务配置,这些配置可用来创建服务器
    srv, err := InitializeWebServer(cfg)
    if err != nil {
        return nil, err
    }

    return &UnionServer{srv: srv}, nil
}


新的创建逻辑中,删除了 NewServerConfig 方法及其中创建各种依赖的逻辑。这些依赖由 wire 注入,从而大大简化了依赖管理的复杂度。如果未来,想了解如何创建 server.Server 类型的示例,只需要阅读 internal/apiserver/wire_gen.go 文件中 InitializeWebServer 函数的实现,即可知道各种依赖的创建方式,从而降低代码的学习成本。


小结(AI 自动生成并人工审核)


本文围绕 Go 项目开发,结合 miniblog 项目,深入探讨了如何通过优化架构设计和工具化手段提升开发效率与代码复用率。


文章借鉴 Kubernetes 的设计理念,通过引入泛型和标准化 Store 层,实现了资源管理的高效复用,减少了代码冗余。同时,借助 OneX 应用基座集中管理共享包,增强了跨项目的协作与复用效率。


此外,文章介绍了内存数据库的实现及部署优化方案,降低了环境依赖,提高了项目的易用性。在项目管理方面,miniblog 通过结构化 Makefile 的设计,整合常用功能,极大提升了开发协作和管理效率。


最后,对于新增 REST 资源的开发,文章提出了规范化流程,并结合工具化支持,显著简化了开发工作,展现了现代化项目开发的高度自动化与高效性。