Home

Awesome

整洁架构 ( Go )

<a id="contents">目录</a>

主要围绕如下几点展开讨论,

  1. 代码规范
    1. 分层思路
    2. 目录结构
    3. 数据分层
  2. 目录结构
    1. 每一层拆分后,水平是否需要在进行分层
    2. 公共的功能放在什么地方
  3. 工程化规范
    1. errors 统一处理规范
    2. API 接口的规范
  4. 其他
    1. 如何保障测试
    2. 错误日志太多:规范日志的输出
    3. 告警太多,导致几乎没人再去关注告警

<br /><hr />

架构思想

工程中大量借鉴了 The Clean ArchitectureDDD 提出的思想,所以先对这 2 个架构的基本思路进行介绍。

整洁架构的主要思想可以参考 The Clean Architecture 这篇文章,DDD 可以参考 阿里技术专家详解 DDD 系列 文章。

整洁架构的好处

引入整洁架构带来的好处主要包括:

  1. 独立于框架:不应该依赖于某种框架,也不需要让系统适应框架。
  2. 可被测试性:可以脱离各种依赖进行测试,比如:UI、数据库、Web 服务等外部因素。
  3. 独立的 UI:对 UI 进行解耦合,不依赖于具体的 UI 实现,也较为容易的可以切换 UI,比如从 Web 界面切换到命令行界面。
  4. 独立于数据库:业务逻辑与具体的数据库解耦,不依赖于具体的数据库,不关心是 MySQL、Oracle 或者其他任何类型的数据库。
  5. 独立于任何外部的依赖:业务逻辑不需要关心任何的外部接口。

整洁架构的模型

上图描述了整洁架构的一个基本模型示例,介绍一下文中的基本概念:

  1. Entities:即实体,类似于 DDD 中的 Domain 层的概念,里面包含对应的业务逻辑
  2. Use Cases:用例,类似于 DDD 中的 Application Service,主要包含各种业务逻辑的编排
  3. 各类依赖和数据渲染层在外层,不会对内部的业务逻辑规则产生影响。
  4. 最重要的是:在图中的依赖关系,内部圈不依赖外层圈
  5. 虽然实际的调用关系是从外层一层一层的调用到最内部的业务逻辑,但是是依靠 依赖注入控制反转 方式进行解耦。

<br /><hr />

代码规范

代码的目录结构参考了 github.com/go-kit/kitgithub.com/go-kratos/kratosgithub.com/golang-standards/project-layout 等工程的代码结构思想,提出下列规范:

从根目录,开始分为:

/api

职责

如何测试

测试关注点

<br />

/cmd

职责

<br />

/internal

强制增加 /internal package,防止其他工程随意引用。

<br />

在这下面可以创建子目录,

子目录职责备注
/internal/serverHTTP Server, gRPC Server 的定义。在这里面主要是对 Server 的生命周期进行管理,这也是很多微服务框架的主要工作之一。比如,对 HTTP Server 的优雅退出进行管理。<br />1、创建 HTTP Server,管理 HTTP Server 的生命周期,包括优雅退出的策略。2、 ( 重点 ) ( 类似于 gRPC ) 使用 Register 的方式将 Server 注入到 /api 中,绑定 Server 与 http router 的关系。
/internal/service1、重点:参数转换,并做简单的参数校验。 2、做业务数据的渲染。 ( 由于没有 BFF,所以将 BFF 的功能放到这一层做,但是会导致这一层的代码膨胀 )service层 --> usecase层 中的 Usercase。
/internal/domain保存 domain 级别的对象,其中包含:domain objectvalue objectdomain service 。 按照 DDD 中的思想,Domain Object 里面包含各自负责领域的业务逻辑。1. 这一层是业务的核心层级。<br /> 2. 这一层按照现在的分层模式,非常独立,不会向上依赖,也不会向下依赖。<br /> 3. 这一层的对象是 Domain Object,需要与 PO (Persistence Object) 或者叫 Data Object 区分。<br /> 4. Domain Object 是带有对应的业务逻辑, PO 只是做个表的简单映射,如果是使用 ORM 工具的话,那么就对应 ORM 映射的对象。<br /> 5. 在这一层下面,可以按照业务的子领域创建各自的 package。<br /> 6. 按照 DDD 的设计思想,本层使用 充血模型<br /> 7. 如何更好的设计领域对象 ( Domain Object ) 强烈推荐参考:ddd的战术篇: application service, domain service, infrastructure service阿里技术专家详解 DDD 系列 第五讲:聊聊如何避免写流水账代码<br /> 8.「不包含」UI 渲染,也「不包含」数据库或 RPC 框架的具体实现。<br /> 9. repo 的依赖,由于是 interface 注入,所以直接 mock 的方式。 ( 可以引入 Go 官方的 gomock )<br /><br />本层的难点:如何定义各种各样的 Domain ObjectDomain Service
/internal/usecaseUse Cases,即 DDD 中的 Application Service,它主要的作用是对 domain 业务的编排<br />若有必要,也可以在该 package 下面定义 子 Usecase
/internal/repo各种数据依赖的具体实现,包括 DB、RPC、缓存等。这里面存放 PO 数据,这些数据就是 简单的表映射这里的对象使用 失血模型 或者 贫血模型
<br />

/pkg

里面定义可以共享出去的工具。由于是可以直接让别人用,这里面的 package 当作基础依赖库使用。既然又是基础依赖库,它里面尽可能的不包含第三方依赖。

<br />

↑ top

<hr />

数据规范

在整个系统中,主要分为下列几个对象:

  1. DTO (Data Transfer Object)- 数据传输对象。对应的是请求、响应的结构体对象。不存在业务逻辑,可以使用 protobuf 来约定。
  2. DO (Domain Object)- 领域对象。包含各个领域的业务逻辑。
  3. PO (Persistant Object)- 持久对象。如果是使用 ORM 的话,那么基本上对应的是表的映射对象。该对象基本上使用贫血模型,除了简单的校验逻辑外,基本上不包含任何业务逻辑。注意:在有的地方,该对象叫做:DO (Data Object)
<br />

分层数据的隔离

在实际开发过程中,有的对象从 DTO 到 DO,或者从 DO 再到 PO 对象很相似,或者一摸一样,但是对应的职责实际上是完全不同,所以必须进行完全隔离。

借用 阿里技术专家详解DDD系列 第三讲 - Repository模式 中的一张图,( 注意:下图中的 DO 实际上代表的是 PO 对象

dto_do_po_convert

为了避免大的结构体对象的赋值的繁琐工作,在做对象组装的时候,可以引入 jinzhu/copier 的工具辅助进行组装。( 这里一定要包含对应的单元测试!!!

<br />

↑ top

<hr />

单元测试

Mock 工具 - mock

由于使用接口注入的方式,所以在做对应模块测试的时候,可以使用 golang/mock 的工具生成对应接口的 mock 实现代码。

为了便于开发过程中的生成,开发了个小工具辅助生成。安装过程为:

  1. 安装 golang/mock
go install github.com/golang/mock/mockgen@v1.6.0
  1. 下载 gmh,运行下列命令即可生成。

# 会在当前目录下,生成 xxx_mock.go 文件。
gmh -src my_interface.go

Mock工具 - mockery

mockery 配合 stretchr/testify/mock 使用,自动生成测试代码。

↑ top

<br /> <hr />

依赖注入

使用 github.com/google/wire 帮助生成代码,完成依赖注入。

安装

go get github.com/google/wire/cmd/wire

使用

进入 cmd/ 目录,运行:

wire
# wire: github.com/xpzouying/go-clean-arch/cmd: wrote /Users/zy/src/zy/go-clean-arch/cmd/wire_gen.go

wire 会根据 wire.go 中的定义,自动生成 wire_gen.go 代码。

↑ top

<br /> <hr />

参考资料