背景

对于大多数 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.Structwire.Bind 的用法看文档就可以了,有点像 Laravel 的接口绑定实现。

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

//go:build !wireinject
// +build !wireinject package inject func InitializeApplication() (*app.Application, func(), error) {
// 内容就是我上面贴的代码!
}

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


文章来源于本人博客,发布于 2020-12-05,原文链接:https://imlht.com/archives/223/

我是如何组织 Go 代码的(目录结构 依赖注入 wire)的更多相关文章

  1. 【Django】基于Django架构网站代码的目录结构

     经典的Django项目源码目录结构 Django在一个项目的目录结构划分方面缺乏必要的规范.在Django的官方文档中并没有给出大型项目的代码建议目录结构,网上的文章也是根据项目的不同结构也有适当的 ...

  2. mybatis学习笔记(六)使用generator生成mybatis基础配置代码和目录结构

    原文:http://blog.csdn.net/oh_mourinho/article/details/51463413 创建maven项目 <span style="font-siz ...

  3. java代码实现目录结构

    今天用java代码来实现.像我们电脑盘符那样的目录结构.在代码开始之前首先.介绍一下.用.java代码实现目录的思想. 第一步:完成基础的.大家想.我们是如何获取文件的.是不是用File类,直接就获取 ...

  4. stm32点亮LED 测试代码及目录结构

    . main.c - 使用PB12, PB13, PB14, PB15, PB5, PB6, PB7 这七个PB口点亮LED. 注意PB3和PB4是特殊口, 直接调用无效. #include &quo ...

  5. 求推荐go语言开发工具及go语言应该以哪种目录结构组织代码?

    go语言的开发工具推荐? go语言开发普通程序及开发web程序的时候,应该以哪种目录结构组织代码? 求推荐go语言开发工具及go语言应该以哪种目录结构组织代码? >> golang这个答案 ...

  6. 前端代码目录结构、常用 piugin、元素补充用法及其它注意事项

    目录结构: app:  .html文件 css: .css文件 script: 脚本文件 plugin: 插件  (此目录放一些通用代码) 注意事项: 1.在IE浏览器下img会显示边框,为了保证兼容 ...

  7. Visual Studio 2013新建工程导入现有代码文件夹并且保持目录结构

    本文提供了一个在Windows环境下使用Visual Studio 2013编辑现有源代码并且保持目录结构的方法.本文使用VS2013中文社区版做示例(本版本为免费版,可在VS官网下载),其他版本的V ...

  8. Cocoa Touch(一)开发基础:Xcode概念、目录结构、设计模式、代码风格

    Xcode相关概念: 概念:project 指一个项目,该项目会负责管理软件产品的全部源代码文件.全部资源文件.相关配置,一个Project可以包含多个Target. 概念:target 一个targ ...

  9. Golang项目目录结构组织

    其实golang的工程管理还是挺简单的,完全使用目录结构还有package名来推导工程结构和构建顺序. 当然,首先要说的是环境变量$GOPATH,项目构建全靠它.这么说吧,想要构建一个项目,就要将这个 ...

  10. 基于gulp编写的一个简单实用的前端开发环境好了,安装完Gulp后,接下来是你大展身手的时候了,在你自己的电脑上面随便哪个地方建一个目录,打开命令行,然后进入创建好的目录里面,开始撸代码,关于生成的json文件请点击这里https://docs.npmjs.com/files/package.json,打开的速度看你的网速了注意:以下是为了演示 ,我建的一个目录结构,你自己可以根据项目需求自己建目

    自从Node.js出现以来,基于其的前端开发的工具框架也越来越多了,从Grunt到Gulp再到现在很火的WebPack,所有的这些新的东西的出现都极大的解放了我们在前端领域的开发,作为一个在前端领域里 ...

随机推荐

  1. vue—一个组件调用另一个组件的methods

    这种方法不常用,项目中有个地方共享数据了,起初没用vuex做,后来有个地方不好解决,这两个组件没有什么关系 1.首先同一个vue实例来调用两个方法.所以可以建立一个中转站. 建立 util.js 中转 ...

  2. Win Pycharm + Airtest + 夜神模拟器 实现APP自动化

    前言: 前面已经讲过了Airtest的简单配置与使用了,相信大家已经对于操作Airtest没有什么问题了(#^.^#) 但是在Airtest IDE中编写代码是有局限性的,而且不能封装Airtest的 ...

  3. 文心一言 VS chatgpt (4)-- 算法导论2.2 1~2题

    一.用O记号表示函数(n ^ 3)/1000-100(n^2)-100n十3. 文心一言: chatgpt: 可以使用大 O 记号表示该函数的渐进复杂度,即: f ( n ) = n 3 1000 − ...

  4. 2022-09-01:字符串的 波动 定义为子字符串中出现次数 最多 的字符次数与出现次数 最少 的字符次数之差。 给你一个字符串 s ,它只包含小写英文字母。请你返回 s 里所有 子字符串的 最大波

    2022-09-01:字符串的 波动 定义为子字符串中出现次数 最多 的字符次数与出现次数 最少 的字符次数之差. 给你一个字符串 s ,它只包含小写英文字母.请你返回 s 里所有 子字符串的 最大波 ...

  5. 2020-12-10:i++是原子操作吗?为什么?

    福哥答案2020-12-10: 不是原子操作.i++分为三个阶段:1.内存到寄存器.2.寄存器自增.3.写回内存.这三个阶段中间都可以被中断分离开.***[评论](https://user.qzone ...

  6. 2020-12-03:mysql中,Heap 表是什么?

    福哥答案2020-12-04:[答案来自此链接:](http://bbs.xiangxueketang.cn/question/605) Heap表,即使用MEMORY存储引擎的表,这种表的数据存储在 ...

  7. hasattr()、getattr()、setattr()函数简介

    hasattr(object, name) 判断object对象中是否存在name属性,当然对于python的对象而言,属性包含变量和方法:有则返回True,没有则返回False:需要注意的是name ...

  8. Golang接收者方法语法糖

    1.概述 在<Golang常用语法糖>这篇博文中我们讲解Golang中常用的12种语法糖,在本文我们主要讲解下接收者方法语法糖. 在介绍Golang接收者方法语法糖前,先简单说下Go 语言 ...

  9. [学习笔记]解决因C#8.0的语言特性导致EFCore实体类型映射的错误

    今天下午在排查一个EF问题时,遇到了个很隐蔽的坑,特此记录. 问题 使用ef执行Insert对象到某表时报错,此对象的Address为空: 不能将值 NULL 插入列 'Address',表 'dbo ...

  10. vue平铺日历组件之按住ctrl、shift键实现跨月、跨年多选日期的功能

    已经好久没有更新过博客了,大概有两三年了吧,因为换了工作,工作也比较忙,所以也就没有时间来写技术博客,期间也一直想写,但自己又比较懒,就给耽误了.今天这篇先续上,下一篇什么时候写,我也不知道,随心所欲 ...