代码分层实践
大约 6 分钟
假设电商项目
一、具体的go项目模块分层
├── application | 应用入口|可以理解为三层架构之中的 UI 表示层
│ ├── admin_service|实现后台管理系统CRUD当前微服务数据需要用到的 RPC 接口
│ │ ├── dto | 数据传输对象实体
│ │ └── grpc | RPC接口实现
│ ├── event | 内置事件 | 比如定时器 | 统一实现一个抽象 interface{ start stop } | 这里注意下就是这个包是不允许有init函数的,禁止自身调用自身,它的调用必须显式地写在 applicition.Init或者编译入口main函数之中
│ └── front_service
│ ├── dtos | 数据传输对象对外的restful接口用到的对象
│ └── http | controller层 | 如果涉及2个领域比如用户中心领域和商户领域 | 在这里将2个领域组装数据 | 这里获取到的各个domain object是一个复杂的对象转
│ └────── init.go | 有一个包变量App结构体 | 首先初始化db\kafka\redis\mongodb\es、再初始化domain service对象,然后以选项模式注入DB对象到领域对象,这里的init也不是用go的init而是Init暴露的方法 ,我这里几乎是禁止用隐藏的init函数
换为api 用的dto
├── config
│ └── files | 里面只有1个.toml文件 | 目前已经弃用 | 配置已经改用从nacos读取
├── domain
│ ├── common | 通用域 | 通用域是被其他域依赖 | 依赖注入也是选项模式注入到其他领域的私有属性 | 域之间是互相独立的互不访问的而通用域则可以注入进其他领域 | 还有支撑域等我这里分的没有那么细致
│ │ ├── entity | 域实体 domain.object 领域对象 | 当前的领域对外暴露的实体
│ │ └── repository | 仓储层
│ │ └──----------- po.go | persistent object 持久化层对象
│ │ └──----------- repository.go | 仓储层对外提供的接口的抽象
│ │ └──----------- repository_realization1.go | 仓储层CRUD的实现一 | 基于MySQL
│ │ └──----------- repository_realization2.go | 仓储层CRUD的实现二 | 基于Oracle | 正常只有1个实现
│ │ service.go | 领域对外暴露的服务 - 同样需要抽象和实现 | 这里有很多的 po 转 do 的操作
│ ├── user
│ │ ├── entity
│ │ └── repository
│ │ service.go
│ ├── good
│ │ ├── entity
│ │ ├── enum
│ │ └── repository
│ │ service.go
├── global | 全局的工具类 - 偏业务 | 可以被domain\event依赖的
│ ├── cache | 缓存key管理
│ ├── enum | 一些通用的枚举值
│ └── router | 路由 | restful
├── cmd | 运行入口 main.go | 运行逻辑大致是 配置加载 > application.Init > rpc.server.register > prometheus/event > run
├── interfaces
└── tools | 工具类 - 剥离业务 - 一小部分特有的 | 大部分的util工具被封装到团队独立的git仓库 | go.mod引用爱用哪个版本用哪个
└── utils
二、其他的私有化Git仓库
- util
团队内的工具类库 - 开发时候以commit id为版本号,上线时候打tag,分支开发模式按照 git flow
- config
仅仅定义所有配置的结构 MySQL\kafka\mongodb\ES
以及配置加载工具 (我这里是 os.env 或者 nacos读取配置)
- proto
.proto文件仓库,开发成员不需要自己手动运行脚本生成.pb.go代码,提交以后流水线会自动生成go文件并生成一个commit id
我这里的proto比较简单的一点,统一仓库涵盖所有的微服务的rpc接口,大一点的微服务架构是每个业务或者每个组自己维护自己的一个git库也就是rpc接口对外服务
rpc接口主要用于其他微服务开发者(rpc的调用方和实现方协作)(当然,禁止出现interface,只有基本数据类型组成的结构体)
这个还是很好用的,定义好抽象以后可以让调用方和实现方同时进行开发
关于proto文件的写法规范、文件放哪里合适啥的
- logger
在编译入口main.go会注入团队内部自用的logger
以前是 logrus 后来底层替换为 zap
微服务应用时候依然是 logger.Errorf \ Info 和官方库log一样的抽象
这个logger具备了统一存储方式、告警功能,比如调用了 logger.Error 企业微信会收到通知,邮件也是
这个还是很好用的
- common_errors
统一定义错误码,比如一个error 一般是 { code_number 错误码整形, code_en 错误英文提示 , code_cn 错误中文提示, code_for_user 给外部用户的错误(屏蔽敏感信息) },作用类似 i18n 那个国际化用的包
不过这个很难用起来,很多同事需求着急的时候是直接在自己开发的git仓库定义错误码类库
- gin-middleware
统一的中间件,目前仅仅封装了统一的登陆验证的 jwt auth 以及简单的敏感字符过滤
- common包
这个和utils我感觉差不多,但是更偏向业务一些,比如团队公用的数据加密,response接口响应
类似goframe的 response.Json \ response.Error \ response.SuccessErcrypt
加密方式、UUID生成工具、ORM、kafkaClient的生成
三、服务拆分
1. 用户
2. 商品
3. 商户
4. 支付
5. 文件中心
6. 后台管理
...
因为有gRPC加上golang编译启动速度极快服务拆分不会对业务开发效率带来太大的阻力,并且对多人开发效率有显著提升
微服务之间依赖的数据库起码是库级别隔离的
比如用户和商品的数据表肯定不是同一个
至于Redis缓存、他们想隔离,但是有时候分布式锁依赖,目前是没隔离
四、git flow 和 CICD (k8s一整套上手也挺困难的)
git flow 下会产生 dev \ feature \ main \release \ hotfix \ tag
ci文件遇到
dev \ feature 分支会自动打包部署到 dev 开发集群
hotfix \ tag 会打包并将镜像push到正式环境的镜像 (手动点击sync就重启部署) pro 正式集群
release 分支会自动打包部署到 testing 测试集群
五、具体一些代码和以前写的不太一样的
1. 领域驱动的 service 依赖的 db对象、缓存对象、还有通用域对象,都是在自己service的私有属性,整个文件所有的操作都是依赖自己的私有属性的,而这些私有属性都是在 applicition.Init 时候以选项模式注入进来的, 这样有一个好处就是,我可以让每个领域驱动依赖的db都不是同一个只需要控制注入的地方,并且对单元测试友好,很多时候我测试一个service接口,这接口就单纯依赖了 Elastic 或者 MySQL ,我只要选择性地注入 Elastic对象就可以完成我的单元测试了,而不是还要考虑为啥redis连不上了
type DomainService struct {
es *common.Elastic
redis *redisapi.RedisApi
db *common.MySQL
mongodb *common.MongoDB
}
2. Controller对象只会初始化1次,以及domian service对象也是仅仅只会初始化1次的,而不是用到的时候 new 一个service对象,多了那么多不需要的GC
3. 依赖层级关系明显,哪些是依赖方,哪些是被依赖方,可以有效避免循环依赖的情况出现
4. 多套一下设计模式
备注
- 日志统一使用zap [日志级别\日志appender\抽象]
- 鉴权用jwt[无状态]
- 业务划分微服务、数据流垂直分层
- 数据分层有仓储层 Persistant Object 持久化对象,领域层 Domain Object ,业务层 Business Object,业务模块输出 DTO