type
status
date
slug
summary
tags
category
icon
password
目录概述Kit Project Layout - 基础库kit 项目必须具备的特点: Service Application Project Layout - 服务layoutService Application ProjectService Application Project - v1Service Application Project - v2Lifecyclewire使用说明API 设计gRPCAPI ProjectAPI Project LayoutAPI Compatibility-兼容性说明向后兼容(非破坏性)的修改向后不兼容(破坏性)的修改API Naming Conventions - API命名API Primitive Fields - 0值API Errors - 错误码
目录概述
- /cmd 本项目的主干。 每个应用程序的目录名应该与你想要的可执行文件的名称相匹配(例如,/cmd/myapp)。 不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg 目录中。如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal 目录中。
- /internal 私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。请注意,这个布局模式是由 Go 编译器本身执行的。有关更多细节,请参阅Go 1.4 release notes。注意,你并不局限于顶级 internal 目录。在项目树的任何级别上都可以有多个内部目录。 你可以选择向 internal 包中添加一些额外的结构,以分隔共享和非共享的内部代码。这不是必需的(特别是对于较小的项目),但是最好有有可视化的线索来显示预期的包的用途。你的实际应用程序代码可以放在 /internal/app 目录下(例如 /internal/app/myapp),这些应用程序共享的代码可以放在 /internal/pkg 目录下(例如 /internal/pkg/myprivlib)。 因为我们习惯把相关的服务,比如账号服务,内部有 rpc、job、admin 等,相关的服务整合一起后,需要区分 app。单一的服务,可以去掉 /internal/myapp。
- /pkg 外部应用程序可以使用的库代码(例如 /pkg/mypubliclib)。其他项目会导入这些库,所以在这里放东西之前要三思:-)注意,internal 目录是确保私有包不可导入的更好方法,因为它是由 Go 强制执行的。/pkg 目录仍然是一种很好的方式,可以显式地表示该目录中的代码对于其他人来说是安全使用的好方法。 /pkg 目录内,可以参考 go 标准库的组织方式,按照功能分类。/internla/pkg 一般用于项目内的 跨多个应用的公共共享代码,但其作用域仅在单个项目工程内。 由 Travis Jeffery 撰写的 I'll take pkg over internal 博客文章提供了 pkg 和 internal 目录的一个很好的概述,以及什么时候使用它们是有意义的。 当根目录包含大量非 Go 组件和目录时,这也是一种将 Go 代码分组到一个位置的方法,这使得运行各种 Go 工具变得更加容易组织。




Kit Project Layout - 基础库
每个公司都应当为不同的微服务建立一个统一的 kit 工具包项目(基础库/框架) 和 app 项目。
基础库 kit 为独立项目,公司级建议只有一个,按照功能目录来拆分会带来不少的管理工作,因此建议合并整合。
by Package Oriented Design
“To this end, the Kit project is not allowed to have a vendor folder. If any of packages are dependent on 3rd party packages, they must always build against the latest version of those dependences.”
kit 项目必须具备的特点:
- 统一
- 标准库方式布局
- 高度抽象
- 支持插件

Service Application Project Layout - 服务layout
- /api API 协议定义目录,xxapi.proto protobuf 文件,以及生成的 go 文件。我们通常把 api 文档直接在 proto 文件中描述。
- /configs 配置文件模板或默认配置。
- /test 额外的外部测试应用程序和测试数据。你可以随时根据需求构造 /test 目录。对于较大的项目,有一个数据子目录是有意义的。例如,你可以使用 /test/data 或 /test/testdata (如果你需要忽略目录中的内容)。请注意,Go 还会忽略以“.”或“_”开头的目录或文件,因此在如何命名测试数据目录方面有更大的灵活性。
- 不应该包含:/src 有些 Go 项目确实有一个 src 文件夹,但这通常发生在开发人员有 Java 背景,在那里它是一种常见的模式。不要将项目级别 src 目录与 Go 用于其工作空间的 src 目录。

Service Application Project
一个 gitlab 的 project 里可以放置多个微服务的app(类似 monorepo)。也可以按照 gitlab 的 group 里建立多个 project,每个 project 对应一个 app。
- 多 app 的方式,app 目录内的每个微服务按照自己的全局唯一名称,比如 “account.service.vip” 来建立目录,如: account/vip/*。
- 和 app 平级的目录 pkg 存放业务有关的公共库(非基础框架库)。如果应用不希望导出这些目录,可以放置到 myapp/internal/pkg 中。

微服务中的 app 服务类型分为4类:interface、service、job、admin。
- interface: 对外的 BFF 服务,接受来自用户的请求,比如暴露了 HTTP/gRPC 接口。
- service: 对内的微服务,仅接受来自内部其他服务或者网关的请求,比如暴露了gRPC 接口只对内服务。
- admin:区别于 service,更多是面向运营测的服务,通常数据权限更高,隔离带来更好的代码级别安全。
- job: 流式任务处理的服务,上游一般依赖 message broker。
- task: 定时任务,类似 cronjob,部署到 task 托管平台中。
cmd 应用目录负责程序的: 启动、关闭、配置初始化等。面向服务生命周期的管理, 服务怎么启动,怎么结束;

Service Application Project - v1
我们老的布局,app 目录下有 api、cmd、configs、internal 目录,目录里一般还会放置 README、CHANGELOG、OWNERS。
- api: 放置 API 定义(protobuf),以及对应的生成的 client 代码,基于 pb 生成的 swagger.json。
- configs: 放服务所需要的配置文件,比如database.yaml、redis.yaml、application.yaml。
- internal: 是为了避免有同业务下有人跨目录引用了内部的 model、dao 等内部 struct。
- server: 放置 HTTP/gRPC 的路由代码,以及 DTO 转换的代码。

DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE 的设计模式。但在这里,泛指用于展示层/API 层与服务层(业务逻辑层)之间的数据传输对象。
项目的依赖路径为: model -> dao -> service -> api,model struct 串联各个层,直到 api 需要做 DTO 对象转换。
- model: 放对应“存储层”的结构体,是对存储的一一隐射。
- dao: 数据读写层,数据库和缓存全部在这层统一处理,包括 cache miss 处理。
- service: 组合各种数据访问来构建业务逻辑。
- server: 依赖 proto 定义的服务作为入参,提供快捷的启动服务全局方法。
- api: 定义了 API proto 文件,和生成的 stub 代码,它生成的 interface,其实现者在 service 中。
service 的方法签名因为实现了 API 的 接口定义,DTO 直接在业务逻辑层直接使用了,更有 dao 直接使用,最简化代码。
DO(Domain Object): 领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。缺乏 DTO -> DO 的对象转换。

Service Application Project - v2
app 目录下有 api、cmd、configs、internal 目录,目录里一般还会放置 README、CHANGELOG、OWNERS
- 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 交互,但是不应处理复杂逻辑。
PO(Persistent Object): 持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么数据表中的每个字段(或若干个)就对应 PO 的一个(或若干个)属性。


我们将 DDD 设计中的一些思想和工程结构做了一些简化,映射到 api、service、biz、data 各层。
在 review PPT 的时候,发现之前 kratos 设计中关于 bussiness error 的定义是放在了 api 层。实际业务错误属于归属于业务领域层。比较合理的应该调整 errros 定义在 biz 中。只需要处理好业务错误到传输(HTTP/gRPC)的转换就行。

松散分层架构(Relaxed Layered System)
层间关系不那么严格。每层都可能使用它下面所有层的服务,而不仅仅是下一层的服务。每层都可能是半透明的,这意味着有些服务只对上一层可见,而有些服务对上面的所有层都可见。
同时在领域驱动设计(DDD)中也采用了继承分层架构(Layering Through Inheritance),高层继承并实现低层接口。我们需要调整一下各层的顺序,并且将基础设施层移动到最高层。
注意:继承分层架构依然是单向依赖,这也意味着领域层、应用层、表现层将不能依赖基础设施层,相反基础设施层可以依赖它们。


- 失血模型
模型仅仅包含数据的定义和 getter/setter 方法,业务逻辑和应用逻辑都放到服务层中。这种类在 Java 中叫 POJO,在 .NET 中叫 POCO。
- 贫血模型 贫血模型中包含了一些业务逻辑,但不包含依赖持久层的业务逻辑。这部分依赖于持久层的业务逻辑将会放到服务层中。可以看出,贫血模型中的领域对象是不依赖于持久层的。
- 充血模型 充血模型中包含了所有的业务逻辑,包括依赖于持久层的业务逻辑。所以,使用充血模型的领域层是依赖于持久层,简单表示就是 UI层->服务层->领域层<->持久层。
- 胀血模型
胀血模型就是把和业务逻辑不想关的其他应用逻辑(如授权、事务等)都放到领域模型中。我感觉胀血模型反而是另外一种的失血模型,因为服务层消失了,领域层干了服务层的事,到头来还是什么都没变。
Lifecycle
Lifecycle 需要考虑服务应用的对象初始化以及生命周期的管理,所有 HTTP/gRPC 依赖的前置资源初始化,包括 data、biz、service,之后再启动监听服务。我们使用
来管理所有资源的依赖注入。为何需要依赖注入?

核心是为了:1、方便测试;2、单次初始化和复用;

wire
手撸资源的初始化和关闭是非常繁琐,容易出错的。
上面提到我们使用依赖注入的思路 DI,结合 google wire,静态的 go generate 生成静态的代码.
可以在很方便诊断和查看,不是在运行时利用 reflection 实现。
使用说明
API 设计
gRPC
gRPC是什么可以用官网的一句话来概括
“A high-performance, open-source universal RPC framework”
- 多语言:语言中立,支持多种语言。
- 轻量级、高性能:序列化支持 PB(Protocol Buffer)和 JSON,PB 是一种语言无关的高性能序列化框架。
- 可插拔
- IDL:基于文件定义服务,通过 proto3 工具生成指定语言的数据结构、服务端接口以及客户端 Stub。
- 设计理念
- 移动端:基于标准的 HTTP2 设计,支持双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量。
- 服务而非对象、消息而非引用:促进微服务的系统间粗粒度消息交互设计理念。
- 负载无关的:不同的服务需要使用不同的消息类型和编码,例如 protocol buffers、JSON、XML和Thrift。
- 流: Streaming API。
- 阻塞式和非阻塞式:支持异步和同步处理在客户端和服务端间交互的消息序列。
- 元数据交换:常见的横切关注点,如认证或跟踪,依赖数据交换。
- 标准化状态码:客户端通常以有限的方式响应 API 调用返回的错误。
不要过早关注性能问题,先标准化。
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
helloworld/helloworld.proto


API Project
为了统一检索和规范API, 可以建立一个统一的bapis仓库, 整合所有内外API.
- API 仓库,方便跨部门协作。
- 版本管理,基于 git 控制。
- 规范化检查,API lint。
- API design review,变更 diff。
- 权限管理,目录 OWNERS

API Project Layout

Protobuf 作为强 schema 的描述文件,也可以方便扩展,是不是用于配置文件定义也可?
API Compatibility-兼容性说明
向后兼容(非破坏性)的修改
- 给 API 服务定义添加 API 接口 从协议的角度来看,这始终是安全的。
- 给请求消息添加字段 只要客户端在新版和旧版中对该字段的处理不保持一致,添加请求字段就是兼容的。
- 给响应消息添加字段 在不改变其他响应字段的行为的前提下,非资源(例如,ListBooksResponse)的响应消息可以扩展而不必破坏客户端的兼容性。即使会引入冗余,先前在响应中填充的任何字段应继续使用相同的语义填充。
向后不兼容(破坏性)的修改
- 删除或重命名服务,字段,方法或枚举值 从根本上说,如果客户端代码可以引用某些东西,那么删除或重命名它都是不兼容的变化,这时必须修改 major 版本号。
- 修改字段的类型 即使新类型是传输格式兼容的,这也可能会导致客户端库生成的代码发生变化,因此必须增加major 版本号。 对于编译型静态语言来说,会容易引入编译错误。
- 修改现有请求的可见行为 客户端通常依赖于 API 行为和语义,即使这样的行为没有被明确支持或记录。 因此,在大多数情况下,修改 API 数据的行为或语义将被消费者视为是破坏性的。如果行为没有加密隐藏,您应该假设用户已经发现它,并将依赖于它。
- 给资源消息添加 读取/写入 字段
API Naming Conventions - API命名
包名为应用的标识(APP_ID),用于生成 gRPC 请求路径,或者 proto 之间进行引用 Message。
文件中声明的包名称应该与产品和服务名称保持一致。带有版本的 API 的软件包名称必须以此版本结尾。
my.package.v1,为 API 目录,定义service相关接口,用于提供业务使用。
// RequestURL: /<package_name>.<version>.<service_name>/{method}
package <package_name>.<version>;


API Primitive Fields - 0值
gRPC 默认使用 Protobuf v3 格式,因为去除了 required 和 optional 关键字,默认全部都是 optional 字段。如果没有赋值的字段,默认会基础类型字段的默认值,比如 0 或者 “”。
Protobuf v3 中,建议使用:
Warpper 类型的字段,即包装一个 message,使用时变为指针。

Protobuf 作为强 schema 的描述文件,也可以方便扩展,是不是用于配置文件定义也可?


API Errors - 错误码
- 使用一小组标准错误配合大量资源
例如,服务器没有定义不同类型的“找不到”错误,而是使用一个标准 google.rpc.Code.NOT_FOUND 错误代码并告诉客户端找不到哪个特定资源。状态空间变小降低了文档的复杂性,在客户端库中提供了更好的惯用映射,并降低了客户端的逻辑复杂性,同时不限制是否包含可操作信息(/google/rpc/error_details)。
2. 错误传播
如果您的 API 服务依赖于其他服务,则不应盲目地将这些服务的错误传播到您的客户端。在翻译错误时,我们建议执行以下操作:
隐藏实现详细信息和机密信息。
调整负责该错误的一方。例如,从另一个服务接收 INVALID_ARGUMENT 错误的服务器应该将 INTERNAL 传播给它自己的调用者。
3. 全局错误码
全局错误码,是松散、易被破坏契约的,基于我们上述讨论的,在每个服务传播错误的时候,做一次翻译,这样保证每个服务 + 错误枚举,应该是唯一的,而且在 proto 定义中是可以写出来文档的。
- 作者:leiax00
- 链接:https://blog.leiax00.cn/article/project-practice
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。