Standard Go Project Layout
https://github.com/golang-standards/project-layout/blob/master/README_zh.md
如果你尝试学习 Go,或者你正在为自己建立一个 PoC 或一个玩具项目,这个项目布局是没啥必要的。从一些非常简单的事情开始(一个 main.go
文件绰绰有余)。当有更多的人参与这个项目时,你将需要更多的结构,包括需要一个 toolkit 来方便生成项目的模板,尽可能大家统一的工程目录布局。
/cmd
本项目的主干。
每个应用程序的目录名应该与你想要的可执行文件的名称相匹配(例如,/cmd/myapp
)。
不要在这个目录中放置太多代码。如果你认为代码可以导入并在其他项目中使用,那么它应该位于 /pkg
目录中。
如果代码不是可重用的,或者你不希望其他人重用它,请将该代码放到 /internal
目录中。
/internal
私有应用程序和库代码。这是你不希望其他人在其应用程序或库中导入代码。请注意,这个布局模式是由 Go 编译器本身执行的。有关更多细节,请参阅 [Go 1.4 release notes](https://golang.org/doc/go1.4)
。注意,你并不局限于顶级 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
/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 - 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 的一个(或若干个)属性。
Lifecycle
Lifecycle 需要考虑服务应用的对象初始化以及生命周期的管理,所有 HTTP/gRPC
依赖的前置资源初始化,包括 data、biz、service,之后再启动监听服务。我们使用 https://github.com/google/wire ,来管理所有资源的依赖注入。为何需要依赖注入?
核心是为了:1、方便测试;2、单次初始化和复用;
Wire
手撸资源的初始化和关闭是非常繁琐,容易出错的。上面提到我们使用依赖注入的思路 DI,结合 google wire,静态的 go generate 生成静态的代码,可以在很方便诊断和查看,不是在运行时利用 reflection 实现。