问题
使用golang进行项目开发时,往往第一件需要考虑的事:项目的目录结构该如何选择?当然,不考虑也可以。那么接下来可以会遇到:
- 杂乱无章的源码文件,比如:一个pkg目录下所有go文件平铺,本人有幸见过这种“优秀代码”并且这个“优秀代码”还被克隆出了另外一个项目。
- 循环引用问题,为了解决循环引用问题,又使用#1的方式存放源码。
- 代码可测试性降低,或者说无法测试。 在接收其他人代码时,有幸又遇到过。程序除非跑起来,否则无法编写单元测试。
分析
什么是杂乱无章?
当然可以认为平铺在代码Pkg目录下,就很棒啊。(默默退出这篇文章的阅读)
问题:1)一个大module上,你无法通过现有的IDE快速阅读源码,比如goland中的structure。2)大部分编程人员的文件命名方式都比较特例独行,你没有办法快速定位源码(目录级Ctrl+F)。3)不美观,当别人接手代码,就能听到中文的优美了。
循环引用问题?
这个问题,我想开发人员都遇到过:A-->B, B-->A,这种问题的情况,往往是代码设计的问题。应该从代码目录结构来禁止开发人员的天马行空。
可测试性?
这一点应该是以上最重要的一点,试想一下:当你写代码的时候,爽,在出现线上问题的时候,到处找环境,找现场,找数据复现的时候,还能爽吗? 要解决的就是代码分层,做到每层可测无依赖 。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
那么,该使用哪样的项目布局解决以上问题?
(举手):那我们可以采用DDD(领域驱动设计)。 这个当然可以,但是有2个非常现实的问题,文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
- 项目开始时,团队内部是否有丰富经验的架构师,丰富经验的领域专业人员。
- 在此基础上,通过前期大量的讨论以及领域划分。 那么恭喜你,快速开发,迭代时长抢占的时机已经过去。
❝文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
个人拙见:当你将系统做了完美时,那么就可以滚蛋了(各种意义上)文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
另外DDD的设计方法论,吸收并应用的成本过高。我认为可以从优秀的项目上吸收经验。是否存在DDD在go语言上的简洁布局?或者GO DDD in action?
看了好几个项目,比较推荐B站的Kratos。根据自己的需求,在kratos上进行了一部分优化。文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
介绍
抛开DDD中的领域,实例,值对象,领域服务,领域事件,聚合等等名词(后面出一篇我看过的比较漂亮的DDD英文的翻译)。
kratos框架: 简单提供了 grpc,http服务的项目结构以及sdk的封装。在我之前工作的框架封装过程中,也参考了其相关理念,使用起来比较顺手。
架构类型分为:分层架构、洋葱架构、六边形架构。 在kratos中采用的是一种分层架构。
单体目录结构如下: github.com/go-kratos/k…文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
csharp
├─api # protobuf 定义
│ └─helloworld
│ └─v1
├─cmd
│ └─server # 命令启动入口
├─configs # 程序启动配置
├─internal
│ ├─biz # 内部领域服务存放地址
│ ├─conf # 配置文件解析地址
│ ├─data # 业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。
│ ├─server # 对外服务创建地址
│ └─service # API接口服务层
└─third_party
├─errors
├─google
│ ├─api
│ └─protobuf
│ └─compiler
├─openapi
│ └─v3
└─validate
调用举例如下:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
在项目结构中比较核心的是biz,承担了每个服务的业务逻辑。
约束:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
- 在领域服务内部,统一都使用领域对象来进行传递。如:service调用biz中的方法,对象从v1.api --> domain。 领域服务调用data进行数据的存储时,将domain--> repo对象。
- 减少项目中全局对象的传递,使用依赖注入原则。(是减少不是一定不)
在使用过程中,又不太方便的地方,在于多层数据转换,service->biz->data。
结合现有业务,大多数领域对象的建模不够完善的情况下,可以使用api.v1中的对象进行传递biz。 即可做biz->data的数据转换。 因为api.v1 作为grpc protobuf自动生成的代码,理论上不存在引用外部模块的情况。
好处:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
- 数据存储发生改变的情况下,只需要data层级中的repo接口(对象方法接口)。存储层只有简单的存储相关操作,不会包含业务逻辑。
- 层级测试,每一层都使用依赖注入的方式,大大方便的数据mock。
依赖注入原则
概念:“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖” ,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。 -- 《引用》
举例有如下引用关系:
在代码中,我们会怎么编码?有几种方式:文章源自灵鲨社区-https://www.0s52.com/bcjc/golangjc/15632.html
- 通过Setter方法传递A类给B。
- 如果A类为全局唯一对象,可将A直接在deal方法中引用。
- 在NewB()结构体的时候,将已创建好的A对象传递进去。
- 声明一个接口对象InterfaceA, B直接使用的为InterfaceA的子方法, A为InterfaceA的实现。
而1,3,4 则是依赖注入的过程。分别对应着:
- setter函数注入。
- 构造函数注入。
- 接口注入。
在Kratos中,可以看到cmd/server/wire.go 文件,即使用了wire库进行依赖注入的操作;
wire库的简要说明
教程请参考:juejin.cn/post/714685…
也可以进一步查看,kratos中的使用。 这里需要注意的坑:
在执行wire命令时,可能会报Unused 错误。为了定位方便,请将每一层ProviderSet变量名,设置为不同名称。
优化
- 增加cobra库的支持
在代码编码过程中,每个微服务难免存在一些脚本或者其他命令操作。高内聚的思想,即微服务都使用cobra 对命令进行封装。
- http库的修改
由于底层的服务为grpc, 上层的http只做为对grpc的转发工作,所以使用grpc-gateway进行替换。
- grpc拦截器的修改
grpc的相关中间件grpc-ecosystem中已有丰富的提供,将其作为替换。
评论