工程项目结构
/cmd 本项目的主干 每个应用程序的目录名应该与你想要的可执行文件的名称相匹配(例如: /cmd/myapp),不要在这个目录中放太多代码。如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将代码放到 /internal 目录中
/internal 私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。
/pkg 外部应用程序可以使用的库代码,其他项目会导入这些库,所以在这里放东西之前要三思。/internal 目录是确保私有包不可导入的更好方法,因为它是由go强制执行的。/pkg目录仍是一种很好的方式,可以显示的表示该目录中的代码对于其他人来说是安全使用的好办法。
服务项目结构
/api api协议定义目录, xxapi.proto protobuf 文件,以及生成的go文件。我们通常把 api 文档直接在 proto 文件中描述。
/configs 配置文件模版或默认配置
/test 额外的外部测试应用程序和测试数据。你可以随时根据需求构造/test目录。对于较大的项目,有一个数据子目录是有意义的。例如,你可以使用/test/data 或者 /test/testdata。请注意,go还会忽略以"." 或者 “_” 开头的目录或文件,因此在如何命名测试数据目录方面有更大的灵活性。
不应该包含:/src 目录 因为go的工作目录的gopath下有个src目录。
微服务中的app 服务类型分为4类:interface, service, job, admin.
interface: 对外的BFF服务, 接受来自用户的请求,比如暴露了 http/grpc 接口。 service: 对内的微服务,仅接受来自内部其他服务或者网光的请求,比如暴露了grpc接口只对内服务。 admin: 区别于service,更多是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别的安全 job: 流式任务处理的服务,上游一般依赖 meesage broker,消息队列 task: 定时任务,类似于 cronjob,部署到task托管平台中 cmd 应用目录负责程序的:启动,关闭,配置初始化等。
DTO (Data Transfer Object):数据传输对象,这个概念来源于J2EE 的设计模式。但在这里,泛指用于展示层API层与服务层之间的传输对象。我们在内部服务定义的结构体,在吐出给api层之后,应该做个deep copy,即,在api层再定义一个结构体,这样可以防止一些不必要的字段暴露出api层。
service application project - v1
项目的依赖路径为: model -> dao -> service -> api, model struct 串联各个层,直到 api 需要做个DTO 对象转换。
mode : 放对应"存储层"的结构体,是对存储的一一印射。 dao: 数据读写层,数据库和缓存全部在这层统一处理,包括 cache miss 处理。 service: 组合各种数据访问来构建业务逻辑。 server: 依赖proto 定义的服务作为入参,提供快捷的启动服务全局方法。 api: 定义了api proto 文件,和生成的stub代码,它生成的interface,其实是现在service里 DO(Domain Object) :领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。v1版本的设计缺乏DTO -> DO 的对象转化
service application project - v2
app目录下有 api,cmd,configs,internal 目录,目录里一般还会有放置 README 等。
internal:是为了避免有同业务下有人跨目录引用了内部的biz,data,service 等内部struct
biz:业务逻辑的组装层,类似于DDD的domain层,data类似于DDD的repo,repo接口在这里定义,使用依赖倒置 原则。 data: 业务数据访问,包括cache,db 等封装,实现了biz的repo接口。我们会把data 与 dao 混淆在一起,data偏重业务的含义,它所要做的是将领域对象重新拿出来,我们去掉了DDD的infra 层 service:实现了api 定义的服务层,类似于DDD的application层,处理DTO到biz领域的实体转换(DTO -> DO), 同时协同各类biz交互,但是不应处理复杂逻辑。
将DDD中的一些思想和工程结构做了一些简化,映射到api,service,biz,data各层。如下图,白色是DDD的思想,深色是工程结构
失血模型:模型仅仅包含数据的定义和get/set 方法,业务逻辑和应用逻辑都放到服务层中。这种类在java中POJO。 贫血模型:贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。可以看出,贫血模型中的领域对象是不依赖于持久层的。 充血模型:充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层的,简单表示就是UI层 -> 服务层 -> 领域层 <->持久层 胀血模型:胀血模型就是把业务逻辑不相关的其他应用逻辑(授权,事务等)都放到了领域模型中。我感觉胀血模型反而是另外一种的失血模型,因为服务层消失了,领域层干了服务层的事,到头来还是什么都没变。
Lifecycle 需要考虑服务应用的对象初始化以及生命周期的管理,所有的http/grpc 依赖的前置资源初始化,包括data,biz,service,之后再启动监听服务。采用依赖注入!
API设计
这里推荐将所有的api设计放在一个git仓库中
api兼容
向后兼容 (非破坏性)的修改
给api服务定义添加api接口:从协议的角度来看,这始终是安全的 给请求消息添加字段:只要客户端在新版跟旧版中对该字段的处理不保持一致,添加请求字段就是兼容的 给相应消息添加字段:在不改变其他响应字段 的行为前提下,非资源的响应消息可以扩展而不必破坏客户端的兼容性。即使会引入冗余,先前在响应中填充的任何字段应继续使用相同的语义填充。
向后不兼容 (破坏性)修改
删除或重命名服务,字段,方法,枚举值:从根本上说,如果客户端代码可以引用某些东西,那么删除或重命名它都是不兼容的变化,这时必须修改major版本号 修改字段的类型:即使新类型是传输格式兼容的,这也可能会导致客户端库生成的代码发生变化,因此必须增加major版本号,对于编译静态语言来说,会容易引入编译错误 修改现有请求的可见行为 给资源消息添加 读取/写入 字段
api 错误码
当我们调用外部的服务的时候,应该将响应的错误码做个转换,如果我们对外部服务的某个错误码感兴趣,我们应该将外部服务的某个错误码转换成我们内部服务的指定错误码。