如何组织Go代码目录结构依赖注入wire使用解析

 更新时间:2023年07月20日 08:48:19   作者:仁扬  
这篇文章主要为大家介绍了如何组织Go代码目录结构依赖注入wire使用解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

背景

对于大多数 Gopher 来说,编写 Go 程序会直接在目录建立 main.go,xxx.go,yyy.go……

不是说不好,对于小型工程来说,简单反而简洁明了,我也提倡小工程没必要整一些花里胡哨的东西。

毕竟 Go 语言作为现代微服务的开发新宠,各个方面都比较自由,没有很多约束。我想,这也是它充满活力的原因。

对于大型工程而言,或者团队协作中,没有明确的规范,只会使得项目越来越凌乱……

因为每个人的心中对代码的管理、组织,对业务的理解不完全是一致的。

我参考了 非官网社区的规范 以及公司的规范,谈谈平时是怎么组织的,希望我的理解,对大家有所帮助。

目录结构示例

.
├── api                          路由与服务挂接
├── cmd                          程序入口,可以有多个程序
│   └── server
│       ├── inject               自动生成依赖注入代码
│       └── main.go
├── config                       配置相关文件夹
├── internal                     程序内部逻辑
│   ├── database
│   │   ├── redis.go 
│   │   └── mysql.go
│   ├── dao                      数据库操作接口/实现
│   │   ├── dao_impls
│   │   │   └── user_impls.go
│   │   └── user.go              用户 DAO 接口
│   ├── svc_impls                服务接口实现
│   │   ├── svc_auth
│   │   └── svc_user
│   └── sdks                     外部 SDK 依赖
└── service                      服务接口定义
        ├── auth.go              认证服务定义
        └── user.go              用户服务定义

面向接口编程

正如你所看到的,我的目录结构将接口和实现分开存放了。

根据依赖倒置原则(Dependence Inversion Principle),对象应依赖接口,而不是依赖实现。

依赖接口带来的好处有很多(当然缺点就是你要多写些代码):

  • 哪天看到某实现有问题,你可以更换一个实现(套娃大法)
  • 编写代码的时候,你可以站在更高的视角看待问题,而不是陷入细节中
  • 编码时,因为接口已经定义好了,你可以一直在当前的模块写下去,不着急写依赖的模块的实现

比如我有个 Deployment 常驻进程管理服务,我是这样定义的:

type Service struct {
    DB                  isql.GormSQL
    DaoGroup            dao.Group
    DaoDeployment       dao.Deployment
    DaoDeploymentStates dao.DeploymentState
    ProcessManager      sdks.ProcessManager
    ServerManager       sdks.ServerManager
    ServerSelector      sdks.ServerSelector
}

该 struct 的成员都是接口。

目前 dao.* 都是在 MySQL 里面,但不排除哪天,我会把 dao.DeploymentState 放到 Redis 存储,此时只需重新实现 CURD 四个借口即可。

因为进程的状态是频繁更新的,数据量大的时候,放 MySQL 不太合适。

我们再看看 ProcessManager,它也是一个 interface:

type ProcessManager interface {
    StartProcess(ctx context.Context, serverIP string, params ProcessCmdArgs) (code, pid int, err error)
    CheckProcess(ctx context.Context, serverIP string, pid int) (err error)
    InfoProcess(ctx context.Context, serverIP string, pid int) (info jobExecutor.ProcessInfoResponse, err error)
    KillProcess(ctx context.Context, serverIP string, pid int) (err error)
    IsProcessNotRunningError(err error) bool
}

我编码的过程中,只要先想好每个模块的入参和出参,ProcessManager 到底要长什么样,我到时候再写!

本地测试时,我也可以写个 mock 版的 ProcessManager,生产的时候是另一个实现,如:

func NewProcessManager(config sdks.ProcessManagerConfig) sdks.ProcessManager {
    config.Default()
    if config.IsDevelopment() {
        return &ProcessManagerMock{config: config}
    }
    return &ProcessManager{config: config}
}

确实是要多写点代码,但是你习惯了之后,你肯定会喜欢上这种方式。

如果你眼尖,你会发现 NewProcessManager 也是依赖倒置的!它依赖 sdks.ProcessManagerConfig 配置:

func GetProcessManagerConfig() sdks.ProcessManagerConfig {
    return GetAcmConfig().ProcessManagerConfig
}

而 GetProcessManagerConfig 又依赖 AcmConfig 配置:

func GetAcmConfig() AcmConfig {
    once.Do(func() {
        err := cfgLoader.Load(&acmCfg, ...)
        if err != nil {
            panic(err)
        }
    })
    return acmCfg
}

也就是说,程序启动时候,可以初始化一个应用配置,有了应用配置,就有了进程管理器,有了进程管理器,就有了常驻进程管理服务……

这个时候你会发现,自己去组织这颗依赖树是非常痛苦的,此时我们可以借助 Google 的 wire 依赖注入代码生成器,帮我们把这些琐事做好。

wire

我以前写 PHP 的时候,主要是使用 Laravel 框架。

wire 和这类框架不同,它的定位是代码生成,也就是说在编译的时候,就已经把程序的依赖处理好了。

Laravel 的依赖注入,在 Go 的世界里对应的是 Uber 的 dig 和 Facebook 的 inject,都是使用 反射 机制实现依赖注入的。

在我看来,我更喜欢 wire,因为很多东西到了运行时,你都不知道具体是啥依赖……

基于代码生成的 wire 对 IDE 十分友好,容易调试。

要想使用 wire,得先理解 Provider 和 Injector:

Provider: a function that can produce a value. These functions are ordinary Go code.

Injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body.

Provider 是一个可以产生值的函数——也就是我们常说的构造函数,上面的 NewProcessManager 就是 Provider。

Injector 可以理解为,当很多个 Provider 组装在一起的时候,可以得到一个管理对象,这个是我们定义的。

比如我有个 func NewApplicaion() *Applicaion 函数,

它依赖了 A、B、C,

而 C 又依赖了我的 Service,

Service 依赖了 DAO、SDK,

wire 就会自动把 *Applicaion 需要 New 的对象都列举出来,

先 NewDao,

然后 NewSDK,

再 NewService,

再 NewC,

最后得到 *Applicaion 返回给我们。

此时,NewApplicaion 就是 Injector,不知道这样描述能不能听懂!

实在没明白的,可以看下代码,这些不是手打的,而是 wire 自动生成的哦~

func InitializeApplication() (*app.Application, func(), error) {
    extend := app.Extend{}
    engine := app.InitGinServer()
    wrsqlConfig := config.GetMysqlConfig()
    gormSQL, cleanup, err := database.InitSql(wrsqlConfig)
    if err != nil {
        return nil, nil, err
    }
    daoImpl := &dao_group.DaoImpl{}
    cmdbConfig := config.GetCmdbConfig()
    rawClient, cleanup2 := http_raw_client_impls.NewHttpRawClient()
    cmdbClient, err := cmdb_client_impls.NewCmdbCli(cmdbConfig, rawClient)
    if err != nil {
        cleanup2()
        cleanup()
        return nil, nil, err
    }
    serverManagerConfig := config.GetServerManagerConfig()
    jobExecutorClientFactoryServer := job_executor_client_factory_server_impls.NewJobExecutorClientFactoryServer(serverManagerConfig)
    serverManager := server_manager_impls.NewServerManager(gormSQL, daoImpl, cmdbClient, serverManagerConfig, jobExecutorClientFactoryServer)
    service := &svc_cmdb.Service{
        ServerManager: serverManager,
    }
    svc_groupService := &svc_group.Service{
        DB:            gormSQL,
        DaoGroup:      daoImpl,
        ServerManager: serverManager,
    }
    dao_deploymentDaoImpl := &dao_deployment.DaoImpl{}
    dao_deployment_stateDaoImpl := &dao_deployment_state.DaoImpl{}
    processManagerConfig := config.GetProcessManagerConfig()
    jobExecutorClientFactoryProcess := job_executor_client_factory_process_impls.NewJobExecutorClientFactoryProcess(serverManagerConfig)
    jobExecutorClientFactoryJob := job_executor_client_factory_job_impls.NewJobExecutorClientFactoryJob(serverManagerConfig)
    processManager := process_manager_impls.NewProcessManager(processManagerConfig, jobExecutorClientFactoryProcess, jobExecutorClientFactoryJob)
    serverSelector := server_selector_impls.NewMultiZonesSelector()
    svc_deploymentService := &svc_deployment.Service{
        DB:                  gormSQL,
        DaoGroup:            daoImpl,
        DaoDeployment:       dao_deploymentDaoImpl,
        DaoDeploymentStates: dao_deployment_stateDaoImpl,
        ProcessManager:      processManager,
        ServerManager:       serverManager,
        ServerSelector:      serverSelector,
    }
    svc_deployment_stateService := &svc_deployment_state.Service{
        DB:                              gormSQL,
        ProcessManager:                  processManager,
        DaoDeployment:                   dao_deploymentDaoImpl,
        DaoDeploymentState:              dao_deployment_stateDaoImpl,
        JobExecutorClientFactoryProcess: jobExecutorClientFactoryProcess,
    }
    authAdminClientConfig := config.GetAuthAdminConfig()
    authAdminClient := auth_admin_client_impls.NewAuthAdminClient(authAdminClientConfig, rawClient)
    redisConfig := config.GetRedisConfig()
    redis, cleanup3, err := database.InitRedis(redisConfig)
    if err != nil {
        cleanup2()
        cleanup()
        return nil, nil, err
    }
    svc_authService := &svc_auth.Service{
        AuthAdminClient: authAdminClient,
        Redis:           redis,
    }
    dao_managersDaoImpl := &dao_managers.DaoImpl{}
    kserverConfig := config.GetServerConfig()
    svc_heartbeatService := &svc_heartbeat.Service{
        DB:                             gormSQL,
        DaoManagers:                    dao_managersDaoImpl,
        ServerConfig:                   kserverConfig,
        JobExecutorClientFactoryServer: jobExecutorClientFactoryServer,
    }
    portalClientConfig := config.GetPortalClientConfig()
    portalClient := portal_client_impls.NewPortalClient(portalClientConfig, rawClient)
    authConfig := config.GetAuthConfig()
    svc_portalService := &svc_portal.Service{
        PortalClient: portalClient,
        AuthConfig:   authConfig,
        Auth:         svc_authService,
    }
    apiService := &api.Service{
        CMDB:            service,
        Group:           svc_groupService,
        Deployment:      svc_deploymentService,
        DeploymentState: svc_deployment_stateService,
        Auth:            svc_authService,
        Heartbeat:       svc_heartbeatService,
        Portal:          svc_portalService,
    }
    ginSvcHandler := app.InitSvcHandler()
    grpcReportTracerConfig := config.GetTracerConfig()
    configuration := config.GetJaegerTracerConfig()
    tracer, cleanup4, err := pkgs.InitTracer(grpcReportTracerConfig, configuration)
    if err != nil {
        cleanup3()
        cleanup2()
        cleanup()
        return nil, nil, err
    }
    gatewayConfig := config.GetMetricsGatewayConfig()
    gatewayDaemon, cleanup5 := pkgs.InitGateway(gatewayConfig)
    application := app.NewApplication(extend, engine, apiService, ginSvcHandler, kserverConfig, tracer, gatewayDaemon)
    return application, func() {
        cleanup5()
        cleanup4()
        cleanup3()
        cleanup2()
        cleanup()
    }, nil
}

wire 怎么用倒是不难,推荐大家使用 Provider Set 组合你的依赖。

可以看下面的例子,新建一个 wire.gen.go 文件,注意开启 wireinject 标签(wire 会识别该标签并组装依赖):

//go:build wireinject
// +build wireinject

package inject

import (
   "github.com/google/wire"
)

func InitializeApplication() (*app.Application, func(), error) {
   panic(wire.Build(Sets))
}

func InitializeWorker() (*worker.Worker, func(), error) {
   panic(wire.Build(Sets))
}

InitializeApplication:这个就是 Injector 了,表示我最终想要 *app.Application,并且需要一个 func(),用于程序退出的时候释放资源,如果中间出现了问题,那就返回 error 给我。

wire.Build(Sets) :Sets 是一个依赖的集合,Sets 里面可以套 Sets:

var Sets = wire.NewSet(
    ConfigSet,
    DaoSet,
    SdksSet,
    ServiceSet,
)
var ServiceSet = wire.NewSet(
    // ...
    wire.Struct(new(svc_deployment.Service), "*"),
    wire.Bind(new(service.Deployment), new(*svc_deployment.Service)),
    wire.Struct(new(svc_group.Service), "*"),
    wire.Bind(new(service.Group), new(*svc_group.Service)),
)

注:wire.Struct 和 wire.Bind 的用法看文档就可以了,有点像 Laravel 的接口绑定实现。

此时我们再执行 wire 就会生成一个 wire_gen.go 文件,它包含 !wireinject 标签,表示会被 wire 忽略,因为是 wire 生产出来的!

//go:build !wireinject
// +build !wireinject

package inject

func InitializeApplication() (*app.Application, func(), error) {
    // 内容就是我上面贴的代码!
}

感谢公司的大神带飞,好记性不如烂笔头,学到了知识赶紧记下来!

以上就是如何组织Go代码目录结构依赖注入wire使用解析的详细内容,更多关于Go目录结构依赖注入wire的资料请关注脚本之家其它相关文章!

相关文章

  • golang的匿名函数和普通函数的区别解析

    golang的匿名函数和普通函数的区别解析

    匿名函数是不具名的函数,可以在不定义函数名的情况下直接使用,通常用于函数内部的局部作用域中,这篇文章主要介绍了golang的匿名函数和普通函数的区别,需要的朋友可以参考下
    2023-03-03
  • Go语言实现动态解析JSON数据的多种方式

    Go语言实现动态解析JSON数据的多种方式

    本文主要介绍了Go语言实现动态解析JSON数据的多种方式,包括map[string]interface{}、interface{}、json.RawMessage及第三方库gjson/mapstructure,具有一定的参考价值,感兴趣的可以了解一下
    2025-05-05
  • 详解Golang如何优雅的终止一个服务

    详解Golang如何优雅的终止一个服务

    后端服务通常会需要创建子协程来进行相应的作业,但进程接受到终止信号或正常结束时,并没有判断或等待子协程执行结束,下面这篇文章主要给大家介绍了关于Golang如何优雅的终止一个服务的相关资料,需要的朋友可以参考下
    2022-03-03
  • Go 泛型和非泛型代码详解

    Go 泛型和非泛型代码详解

    Go 在 1.17 中支持泛型,但是默认未开启;1.18 中会正式支持泛型,下面文章内容小编将给大家讲解Go 语言中的泛型和非泛型并且附上代码详解,刚兴趣的小伙伴请参考下面文章的具体内容
    2021-10-10
  • go编程中go-sql-driver的离奇bug解决记录分析

    go编程中go-sql-driver的离奇bug解决记录分析

    这篇文章主要为大家介绍了go编程中go-sql-driver的离奇bug解决记录分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • Golang配置管理库 Viper的教程详解

    Golang配置管理库 Viper的教程详解

    这篇文章主要介绍了Golang 配置管理库 Viper,使用 viper 能够很好的去管理你的配置文件信息,比如数据库的账号密码,服务器监听的端口,你可以通过更改配置文件去更改这些内容,而不用定位到那一段代码上去,提高了开发效率,需要的朋友可以参考下
    2022-05-05
  • Go Map并发冲突预防与解决

    Go Map并发冲突预防与解决

    这篇文章主要为大家介绍了Go Map并发冲突预防与解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Golang使用archive/zip包实现ZIP压缩与解压

    Golang使用archive/zip包实现ZIP压缩与解压

    Golang 中的 archive/zip 包用于处理 ZIP 格式的压缩文件,提供了一系列用于创建、读取和解压缩 ZIP 格式文件的函数和类型,使用起来非常方便,下面就跟随小编一起了解一下具体使用方法吧
    2023-08-08
  • 聊聊go xorm生成mysql的结构体问题

    聊聊go xorm生成mysql的结构体问题

    这篇文章主要介绍了go xorm生成mysql的结构体问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2022-03-03
  • Go语言实现牛顿法求平方根函数的案例

    Go语言实现牛顿法求平方根函数的案例

    这篇文章主要介绍了Go语言实现牛顿法求平方根函数的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12

最新评论