go-clean-template源码分析

April 3, 2022

简介

go-clean-template是一个golang的微服务模板,按照Robert Martin《代码整洁之道》的各原则来实现。业务方可以使用该模板来扩展构建自己的微服务。

提供的能力项包括:

  • 按照DIP、ISP等原则来组织框架层、业务逻辑层以及数据层等代码实现。
  • 配置文件解析,包括端口号、日志、PG等RabbitMQ配置。
  • 集成gin框架,提供了几个HTTP接口demo实现。
  • 集成了RabbitMQ RPC,提供了1个接口的demo实现。
  • 集成了日志、Swagger、K8s probe、Prometheus上报等能力。

本项目主要是作为模板来演示如何构建可扩展、易于维护的微服务,因此在实现上比较简单,业务方可以根据实际需求进一步改造扩充。

工程架构

启动方式

README中给出了docker的构建方式,也可以将相关代码copy出来之后自行go build构建。

注意服务依赖RabbitMQ、PostgreSQL,启动前需要搭建对应的容器或者实例。

目录结构

  • cmd: cmd处理逻辑。
  • config:配置解析
  • docker-compose.yml: Image构建
  • internal:内部服务的代码逻辑,与框架、pkg等实现上完全隔离。
    • app: app的具体实现,包括配置解析、业务逻辑注入、http/RPC启动、signal响应以及退出逻辑
    • controller:对外的API层实现,包括http、RPC接口的路由以及实现。
    • entity:entity层,通常用于公共数据结构的声明。
    • usecase:业务逻辑实现,采用了DIP原则,内容包括了接口声明、业务逻辑实现以及单侧等。
  • pkg:独立的模块
    • http server:http服务启动
    • logger:日志实现,基于zerolog
    • postgres:封装了golang postgre实现
    • rabbitmq:封装rabbit RPC。

核心设计

代码整洁之道的不少原则在项目中都有体现

ISP:接口隔离原则

定义:任何层次的软件设计如果依赖了它并不需要的东西,就会带来意料之外的麻烦。

User case中Repo(缓存存取数据)和WebAPI(拉取外部数据),虽然都属于业务逻辑流程中的一部分,但是在实现上完全隔离,相互间无依赖关系。数据存储不依赖API的实现。

LSP:里氏替换原则

定义1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。

定义2:所有引用基类的地方必须能透明地使用其子类的对象

pkg中的各个独立模块的实现就有LSP的体验,比如log的实现,log的内部实现上依赖了zerolog,但是zerolog没有扩散到代码的其他部分。理论上log内部实现可以被其他log lib替换,比如glog、zap等,而且上层无需感知。

DIP:依赖倒置

定义:源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。

user case的实现就是典型的DIP原则。

  1. 声明user case的interface。在代码的其他模块仅仅依赖interface,不依赖interface的具体实现。
Loading...
  1. app启动前将业务逻辑的具体实现注入到user case中,供其他业务逻辑使用。
Loading...

通过New构造函数,将依赖注入到业务逻辑的结构中. 这使得业务逻辑独立于外部接口。业务逻辑内部实现之间也也是通过Interface来交互,没有直接依赖相互的实现。这样实现后续在业务逻辑的实现变更时,比如更换WebAPI依赖的后台接口,或者将数据存储从Postgre迁移到Mysql时,对其他模块没有任何影响。

DIP的另外一大收益为单测,可以自动生成mock,以便进行单元测试。

Loading...

总结

  • 代码整体上可以分为内网两层,外传主要包括路由、http/RCP API处理、以及PG、MQ等组件的封装。内层主要为业务逻辑的实现。
  • 各层之间基本没有直接依赖关系,尽量是通过各种Interface进行通信

其他实践

  • 配置解析:使用了cleanenv,一个比较简单的实现。个人感觉可以使用vip等替换。
  • 文档中推荐后续采用wire来管理依赖关系。没有实际研究过wire,初步感觉有点鸡肋。但是不确定在大型项目中是否有较大收益,后续可以关注。
  • 业务逻辑的实现(usercase/)中,按应用领域了进行了分组。

Reference

See all postsSee all posts