SoFunction
Updated on 2025-03-05

How to organize Go code directory structure dependency injection wire usage parsing

background

For most Gophers, writing Go programs will be created directly in the directory,...

It’s not that bad, for small projects, they are simple but concise and clear. I also advocate that there is no need to make some fancy things in small projects.

After all, as the new favorite of modern microservice development, Go language is relatively free in all aspects and does not have many constraints. I think that's why it's full of vitality.

For large-scale projects, or in teamwork, there is no clear specification, which will only make the project more and more messy...

Because everyone's understanding of code management and organization and business is not completely consistent in their minds.

I've referencedStandards for non-official communitiesAs for the company's specifications, let's talk about how it is organized in normal times. I hope my understanding will be helpful to everyone.

Example directory structure

.
├── api                          Routing and service hookup
├── cmd                          Program entry,There can be multiple programs
│   └── server
│       ├── inject               Automatically generate dependency injection code
│       └── 
├── config                       Configure related folders
├── internal                     Program internal logic
│   ├── database
│   │   ├──  
│   │   └── 
│   ├── dao                      Database operation interface/accomplish
│   │   ├── dao_impls
│   │   │   └── user_impls.go
│   │   └──               user DAO interface
│   ├── svc_impls                服务interfaceaccomplish
│   │   ├── svc_auth
│   │   └── svc_user
│   └── sdks                     external SDK rely
└── service                      服务interface定义
        ├──               Certification Service Definition
        └──               user服务定义

Interface-oriented programming

As you can see, my directory structure stores the interface and implementation separately.

According to the Dependence Inversion Principle, objects should rely on interfaces, not on implementations.

There are many benefits of relying on interfaces (of course, the disadvantage is that you have to write more code):

  • One day you see that there is a problem with an implementation, you can change it (mastiff doll method)
  • When writing code, you can look at the problem from a higher perspective instead of getting stuck in the details
  • When encoding, because the interface has been defined, you can keep writing in the current module and do not rush to write the implementation of the dependent module.

For example, I have a Deployment resident process management service, which I define as follows:

type Service struct {
    DB                  
    DaoGroup            
    DaoDeployment       
    DaoDeploymentStates 
    ProcessManager      
    ServerManager       
    ServerSelector      
}

The members of the struct are interfaces.

Currently, dao.* is all in MySQL, but it is not ruled out that one day, I will put it in Redis storage. At this time, I only need to re-implement the four excuses of CURD.

Because the status of the process is updated frequently, it is not appropriate to put MySQL when the data volume is large.

Let's take a look at ProcessManager, which is also an interface:

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

During my coding process, I just need to think about the incoming and outgoing parameters of each module first, and I will write it then!

During local testing, I can also write a mock version of ProcessManager, which is another implementation during production, such as:

func NewProcessManager(config )  {
    ()
    if () {
        return &ProcessManagerMock{config: config}
    }
    return &ProcessManager{config: config}
}

It is indeed necessary to write more code, but once you get used to it, you will definitely like this method.

If you have sharp eyes, you will find that NewProcessManager also relies on inversion! It depends on configuration:

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

GetProcessManagerConfig relies on AcmConfig configuration:

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

In other words, when the program starts, an application configuration can be initialized. With the application configuration, there is a process manager, and with the process manager, there is a resident process management service...

At this time, you will find that it is very painful to organize this dependency tree yourself. At this time, we can use Google's wire dependency injection code generator to help us do these trivial matters well.

wire

When I wrote PHP, I mainly used the Laravel framework.

Wire is different from this type of framework. Its positioning is code generation, which means that the program's dependencies have been processed when compiling.

Laravel's dependency injection, in the Go world, corresponds to Uber's dig and Facebook's inject, both of which are used.reflectionThe mechanism implements dependency injection.

In my opinion, I prefer wire because when many things are running, you don’t know what specific dependencies are...

Wire based on code generation is very IDE friendly and easy to debug.

To use wire, you must first understand Provider and 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 is a function that can produce values ​​- which is what we often call a constructor. The NewProcessManager above is Provider.

Injector can be understood as that when many providers are assembled together, a management object can be obtained, which is what we define.

For example, I have onefunc NewApplicaion() *ApplicaionFunction,

It relies on A, B, C,

And C relies on my Service,

Service relies on DAO and SDK.

wire will automatically list all objects that *Applicaion needs New.

First NewDao,

Then NewSDK,

NewService,

Again NewC,

Finally get *Applicaion and return it to us.

At this time, NewApplicaion is the Injector, I don’t know if I can understand this description!

If you really don't understand, you can check the code. These are not hand-made, but are automatically generated by wire~

func InitializeApplication() (*, func(), error) {
    extend := {}
    engine := ()
    wrsqlConfig := ()
    gormSQL, cleanup, err := (wrsqlConfig)
    if err != nil {
        return nil, nil, err
    }
    daoImpl := &dao_group.DaoImpl{}
    cmdbConfig := ()
    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 := ()
    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 := ()
    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 := ()
    authAdminClient := auth_admin_client_impls.NewAuthAdminClient(authAdminClientConfig, rawClient)
    redisConfig := ()
    redis, cleanup3, err := (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 := ()
    svc_heartbeatService := &svc_heartbeat.Service{
        DB:                             gormSQL,
        DaoManagers:                    dao_managersDaoImpl,
        ServerConfig:                   kserverConfig,
        JobExecutorClientFactoryServer: jobExecutorClientFactoryServer,
    }
    portalClientConfig := ()
    portalClient := portal_client_impls.NewPortalClient(portalClientConfig, rawClient)
    authConfig := ()
    svc_portalService := &svc_portal.Service{
        PortalClient: portalClient,
        AuthConfig:   authConfig,
        Auth:         svc_authService,
    }
    apiService := &{
        CMDB:            service,
        Group:           svc_groupService,
        Deployment:      svc_deploymentService,
        DeploymentState: svc_deployment_stateService,
        Auth:            svc_authService,
        Heartbeat:       svc_heartbeatService,
        Portal:          svc_portalService,
    }
    ginSvcHandler := ()
    grpcReportTracerConfig := ()
    configuration := ()
    tracer, cleanup4, err := (grpcReportTracerConfig, configuration)
    if err != nil {
        cleanup3()
        cleanup2()
        cleanup()
        return nil, nil, err
    }
    gatewayConfig := ()
    gatewayDaemon, cleanup5 := (gatewayConfig)
    application := (extend, engine, apiService, ginSvcHandler, kserverConfig, tracer, gatewayDaemon)
    return application, func() {
        cleanup5()
        cleanup4()
        cleanup3()
        cleanup2()
        cleanup()
    }, nil
}

It is not difficult to use wire. We recommend that you use Provider Set to combine your dependencies.

You can see the following example, create a new file, and pay attention to opening the wireinject tag (wire will recognize the tag and assemble the dependency):

//go:build wireinject
// +build wireinject

package inject

import (
   "/google/wire"
)

func InitializeApplication() (*, func(), error) {
   panic((Sets))
}

func InitializeWorker() (*, func(), error) {
   panic((Sets))
}

InitializeApplication: This is Injector, which means I want it in the end*, and a func() is needed to release resources when the program exits. If there is a problem in the middle, then return error to me.

(Sets): Sets is a dependency collection, and Sets can be arranged in Sets:

var Sets = (
    ConfigSet,
    DaoSet,
    SdksSet,
    ServiceSet,
)
var ServiceSet = (
    // ...
    (new(svc_deployment.Service), "*"),
    (new(), new(*svc_deployment.Service)),
    (new(svc_group.Service), "*"),
    (new(), new(*svc_group.Service)),
)

Note:andThe usage of   is enough to read the documentation, which is a bit like Laravel's interface binding implementation.

At this time, we execute wire and generate a wire_gen.go file, which contains!wireinjectTags indicate that they will be ignored by wire because they are produced by wire!

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

package inject

func InitializeApplication() (*, func(), error) {
    // The content is the code I posted above!}

Thanks to the great master of the company for helping you. You are not as good as a bad pen. If you learn the knowledge, remember it quickly!

The above is the detailed content of how to organize the Go code directory structure dependency injection wire usage analysis. For more information about Go directory structure dependency injection wire, please follow my other related articles!