Golang Project Layout 最佳实践
鲍勃叔叔干净的架构(Uncle Bob)
依赖规则
- 同心圆代表软件的不同领域。
- 一般来说,你走得越远,软件的层次就越高。外圈是机制。内圈是政策。
使这个架构工作的最重要的规则是依赖规则
。这条规则说源代码依赖只能指向内部
。内圈中的任何人都无法对外圈中的事物一无所知。特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类。变量或任何其他命名的软件实体。
同样,在外圈中使用的数据格式不应该被内圈使用,特别是如果这些格式是由外圈中的框架生成的。我们不希望外圈的任何东西影响内圈。
实体(entity
)
实体封装了企业范围的业务规则。实体可以是具有方法的对象,也可以是一组数据结构和函数。只要实体可以被企业中的许多不同应用程序使用,这并不重要。
如果您没有企业,而只是编写单个应用程序,则这些实体是应用程序的业务对象。它们封装了最一般和高级的规则。当外部变化时,它们最不可能改变。例如,您不会期望这些对象受到页面导航或安全性更改的影响。对任何特定应用程序的操作更改都不应影响实体层。
使用案例(usecase
)
此层中的软件包含特定于应用程序的业务规则
。它封装并实现了系统的所有用例。这些用例协调进出实体的数据流,并指示这些实体使用其企业范围
的业务规则来实现用例的目标。
我们预计此层中的更改不会影响实体。我们也不希望此层受到外部性更改(如数据库、UI 或任何通用框架)的影响。这一层与此类问题隔离开来。
但是,我们确实预计对应用程序操作的更改将影响用例,从而影响该层中的软件。如果用例的细节发生变化,那么这一层中的一些代码肯定会受到影响。
接口适配器(interface adapter
)
该层中的软件是一组适配器,可将数据从最适合用例和实体的格式转换为最适合某些外部机构(如数据库或 Web)的格式。例如,正是这一层将完全包含 GUI
的 MVC
架构。Presenters
、Views
和 Controllers
都属于这里。模型可能只是从控制器传递到用例,然后从用例返回到演示者和视图的数据结构。
类似地,在这一层中,数据从对实体和用例最方便的形式转换为对正在使用的任何持久性框架最方便的形式。即数据库。这个圈子内的任何代码都不应该对数据库有任何了解。如果数据库是 SQL 数据库,那么所有的 SQL 都应该限制在这一层,特别是限制在这一层与数据库有关的部分。
在这一层中还有任何其他适配器,用于将数据从某种外部形式(例如外部服务)转换为用例和实体使用的内部形式
框架和驱动器(Frameworks and Drivers)
最外层一般由框架和工具组成,如数据库、Web 框架等。一般在这一层你不会写太多代码,除了向内与下一个循环通信的胶水代码。
这一层是所有细节的所在。网络是一个细节。数据库是一个细节。我们把这些东西放在外面,它们不会造成什么伤害。
圈和边界
源代码依赖项总是指向内部。随着向内移动,抽象级别会增加。最外圈是低层次的具体细节。随着您向内移动,软件变得更加抽象,并封装了更高级别的策略。最内圈是最普通的。
通常,跨越边界的数据是简单的数据结构。如果您愿意,可以使用基本结构或简单的数据传输对象。或者数据可以只是函数调用中的参数。或者你可以将它打包成一个 hashmap,或者将它构造成一个对象。重要的是,隔离的、简单的数据结构可以跨越边界传递。我们不想欺骗和传递实体或数据库行。我们不希望数据结构有任何违反依赖规则的依赖。
例如,许多数据库框架返回方便的数据格式以响应查询。我们可以称其为 Row Structure。我们不想跨边界向内传递该行结构。这将违反依赖规则,因为它会迫使内圈了解外圈。
所以当我们越界传递数据时,总是采用最方便内圈的形式。
结论
遵守这些简单的规则并不难,并且会为您省去很多麻烦。通过将软件分层并遵守依赖规则,您将创建一个本质上可测试的系统,并具有所有暗示的好处。当系统的任何外部部件(如数据库或 Web 框架)过时时,您可以轻松替换那些过时的元素。
从 kratos 框架入手
|
|
应用目录
/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 一般用于项目内的 跨多个应用的公共共享代码,但其作用域仅在单个项目工程内。
服务应用目录
/api
API 协议定义目录,services.proto protobuf 文件,以及生成的 go 文件。我们通常把 api 文档直接在 proto 文件中描述。
/configs
配置文件模板或默认配置。
/test
额外的外部测试应用程序和测试数据。你可以随时根据需求构造 /test 目录。对于较大的项目,有一个数据子目录是有意义的。例如,你可以使用 /test/data 或 /test/testdata (如果你需要忽略目录中的内容)。请注意,Go 还会忽略以 “.” 或 “_” 开头的目录或文件,因此在如何命名测试数据目录方面有更大的灵活性。
服务内部目录
Application 目录下有 api、cmd、configs、internal、pkg 目录,目录里一般还会放置 README、CHANGELOG、OWNERS。internal 是为了避免有同业务下有人跨目录引用了内部的 data、biz、service、server 等内部 struct。
data
业务数据访问,包含 cache、db 等封装,实现了 biz 的 repo 接口。我们可能会把 data 与 dao 混淆在一起,data 偏重业务的含义,它所要做的是将领域对象重新拿出来,我们去掉了 DDD 的 infra层。
biz
业务逻辑的组装层,类似 DDD 的 domain 层,data 类似 DDD 的 repo,repo 接口在这里定义,使用依赖倒置的原则。
service
实现了 api 定义的服务层,类似 DDD 的 application 层,处理 DTO 到 biz 领域实体的转换(DTO -> DO),同时协同各类 biz 交互,但是不应处理复杂逻辑。
server
为http和grpc实例的创建和配置,以及注册对应的 service 。
总结
在实际 go 项目开发中,一定要灵活运用,当然也可以完全不按照这样架构分层、包设计的规则,一切以项目的大小、业务的复杂度、个人专业技能认知的广度和深度、时间的紧迫度为准。
github上优秀的项目
go-clean-arch
此项目有 4 个域层
- 模型层
- 存储库层
- 用例层
- 交付层
go-clean-template
服务流向
|
|