- Created by 郭强, last modified by 海亮 on Feb 05, 2024
工程目录设计是代码分层设计的进一步落地,建议您先仔细阅读:代码分层设计
这是GoFrame
框架针对业务项目的目录设计,主体的思想来源于三层架构,但在具体实现中,对其进行了一定的改进和细化使其更符合工程实践和时代进步。
一、工程目录结构
GoFrame
业务项目基本目录结构如下(以Single Repo
为例):
/ ├── api ├── hack ├── internal │ ├── cmd │ ├── consts │ ├── controller │ ├── dao │ ├── logic │ ├── model │ | ├── do │ │ └── entity │ └── service ├── manifest ├── resource ├── utility ├── go.mod └── main.go
目录/文件名称 | 说明 | 描述 |
---|---|---|
api | 对外接口 | 对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以api/xxx/v1... 存在。 |
hack | 工具脚本 | 存放项目开发工具、脚本等内容。例如,CLI 工具的配置,各种shell/bat 脚本等文件。 |
internal | 内部逻辑 | 业务逻辑存放目录。通过Golang internal 特性对外部隐藏可见性。 |
- cmd | 入口指令 | 命令行管理目录。可以管理维护多个命令行。 |
- consts | 常量定义 | 项目所有常量定义。 |
- controller | 接口处理 | 接收/解析用户输入参数的入口/接口层。 |
| 数据访问 | 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法 |
- logic | 业务封装 | 业务逻辑封装管理,特定的业务逻辑实现和封装。往往是项目中最复杂的部分。 |
- model | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。 |
- do | 领域对象 | 用于dao 数据操作中业务模型与实例模型转换,由工具维护,用户不能修改。 |
- entity | 数据模型 | 数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改。 |
- service | 业务接口 | 用于业务模块解耦的接口定义层。具体的接口实现在logic 中进行注入。 |
manifest | 交付清单 | 包含程序编译、部署、运行、配置的文件。常见内容如下: |
- config | 配置管理 | 配置文件存放目录。 |
- docker | 镜像文件 | Docker 镜像相关依赖文件,脚本文件等等。 |
- deploy | 部署文件 | 部署相关的文件。默认提供了Kubernetes 集群化部署的Yaml 模板,通过kustomize 管理。 |
- protobuf | 协议文件 | GRPC 协议时使用的protobuf 协议定义文件,协议文件编译后生成go 文件到api 目录。 |
resource | 静态资源 | 静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中。 |
go.mod | 依赖管理 | 使用Go Module 包管理的依赖描述文件。 |
main.go | 入口文件 | 程序入口文件。 |
对外接口
对外接口包含两部分:接口定义(api
)+接口实现(controller
)。
服务接口的职责类似于三层架构设计中的UI
表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用 service
实现业务逻辑处理。
接口定义 - api
api
包用于与客户端约定的数据结构输入输出定义,往往与具体的业务场景强绑定。
接口实现 - controller
controller
用于接收api
的输入,可以直接在controller
中实现业务逻辑,或者调用一个或多个service
包实现业务逻辑,将执行结果封装为约定的api
输出数据结构。
业务实现
业务实现包含两部分:业务接口(service
)+业务封装(logic
)。
业务实现的职责类似于三层架构设计中的BLL
业务逻辑层,负责具体业务逻辑的实现以及封装。
在后续的章节介绍中,我们会将业务实现统一称作service
,大家注意它其实包含两部分即可。
业务接口 - service
service
包用于解耦业务模块之间的调用。业务模块之间往往不会直接调用对应的业务模块资源来实现业务逻辑,而是通过调用service
接口。service
层只有接口定义,具体的接口实现注入在各个业务模块中。
业务封装 - logic
logic
包负责具体业务逻辑的实现以及封装。项目中各个层级代码不会直接调用logic
层的业务模块,而是通过service
接口层来调用。
结构模型
model
包的职责类似于三层架构中的Model
模型定义层。模型定义代码层中仅包含全局公开的数据结构定义,往往不包含方法定义。
这里需要注意的是,这里的model
不仅负责维护数据实体对象(entity
)结构定义,也包括所有的输入/输出数据结构定义,被api/dao/service
共同引用。这样做的好处除了可以统一管理公开的数据结构定义,也可以充分对同一业务领域的数据结构进行复用,减少代码冗余。
数据模型 - entity
与数据集合绑定的程序数据结构定义,通常和数据表一一对应。
业务模型 - model
与业务相关的通用数据结构定义,其中包含大部分的方法输入输出定义。
数据访问 - dao
dao
包的职责类似于三层架构中的DAL
数据访问层,数据访问层负责所有的数据访问收口。
三层架构设计与框架代码分层映射关系
二、请求分层流转
cmd
cmd
层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动server
监听、阻塞运行程序直至server
退出。
api
上层server
服务接收客户端请求,转换为api
中定义的Req
接收对象、执行请求参数到Req
对象属性的类型转换、执行Req
对象中绑定的基础校验并转交Req
请求对象给controller
层。
controller
controller
层负责接收Req
请求对象后做一些业务逻辑校验,可以直接在controller
中实现业务逻辑,或者调用一个或多个service
实现业务逻辑,将执行结果封装为约定的Res
数据结构对象返回。
model
model
层中管理了所有的业务模型,service
资源的Input/Output
输入输出数据结构都由model
层来维护。
service
service
是接口层,用于解耦业务模块,service
没有具体的业务逻辑实现,具体的业务实现是依靠logic
层注入的。
logic
logic
层的业务逻辑需要通过调用dao
来实现数据的操作,调用dao
时需要传递do
数据结构对象,用于传递查询条件、输入数据。dao
执行完毕后通过Entity
数据模型将数据结果返回给service
层。
dao
dao
层通过框架的ORM
抽象层组件与底层真实的数据库交互。
三、常见问题解答
框架是否支持常见的MVC
开发模式
当然!
作为一款模块化设计的基础开发框架,GoFrame
不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于MVC
模式中常见的模板渲染开发。相比较MVC
开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。
当api
与model
层存在重复数据结构时如何维护
在api
中定义的数据结构是对外使用的,与具体的业务场景绑定(如具体的页面交互逻辑、单一的接口功能),数据结构是由上层展示层前置决定的;model
中定义的数据结构是服务内部使用的,数据结构是在接口实现和抽象的过程中才能定义的,并且model
中的数据结构可以随意在内部修改而并不会影响对外api
接口的兼容性。
注意model
中的数据结构不应该直接暴露给外部使用,并且在框架的工程设计中刻意将model
目录放到了internal
目录下。也不应该在api
层中对model
中的数据结构进行别名类型定义供外部访问。一旦将model
中的数据结构应用到了api
层中,内部model
数据结构的修改会直接影响到api
接口的兼容性。
如果两者出现重复的数据结构(甚至常量、枚举类型),建议将数据结构定义到api
层中。服务内部逻辑可以直接访问api
层的数据结构。model
层的数据结构也可以直接引用api
层的数据结构,但是反之则不行。
我们来看一个示例,便于更好地理解:
如何清晰界定和管理service
和controller
的分层职责
controller
层处理Req/Res
外部接口请求。负责接收、校验请求参数,可以直接在controller
中实现业务处理逻辑,或者调用一个或多个 service
来实现业务逻辑处理,将执行结果封装为约定的api
输出数据结构返回。service
层处理Input/Output
内部方法调用。负责内部可复用的业务逻辑封装,封装的方法粒度往往比较细。
通常来讲,开发接口时只需要编写controller
层中的接口实现业务逻辑即可,当存在重复代码逻辑时,再从各个controller
接口实现逻辑中抽象沉淀到service
层。如果从controller
层直接透传Req
对象给service
,同时service
直接返回Res
数据结构对象,该方法也就与外部接口耦合,仅面向外部接口服务,难以复用,这样会增加技术债务成本。
如何清晰界定和管理service
和dao
的分层职责
这是一个很经典的问题。
痛点:
常见的,开发者把数据相关的业务逻辑实现封装到了dao
代码层中,而service
代码层只是简单的dao
调用,这么做的话会使得原本负责维护数据的dao
层代码越来越繁重,反而业务逻辑service
层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到dao
还是service
中?
业务逻辑其实绝大部分时候都是对数据的CURD
处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在dao
层中,业务逻辑的改变其实会频繁对dao
层的代码产生修改。例如:数据查询需求,在初期的时候可能只是简单的逻辑,目前代码放到dao
好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的dao
代码,同时service
代码也可能同时做更新。原本仅限于service
层的业务逻辑代码职责与dao
层代码职责模糊不清、耦合较重,原本只需要修改service
代码的需求变成了同时修改service
+dao
,使得项目中后期的开发维护成本大大增加。
建议:
我们的建议。dao
层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。业务逻辑、包括看似只是简单的数据操作的逻辑都应当封装到service
中。service
中包含多个业务模块,每个模块独自管理自己的dao
对象。理想情况下,service
与service
之间通过相互调用方法来实现数据通信,而不是随意去调用其他service
模块的dao
对象。
为什么要使用internal
目录包含业务代码
internal
目录是Golang
语言专有的特性,防止同级目录外的其他目录引用其下面的内容。业务项目中存在该目录的目的,是避免若项目中存在多个子项目(特别是大仓管理模式时),多个项目之间无限制随意访问,造成难以避免的多项目不同包之间耦合。
- No labels
44 Comments
qiezi
有没有前后端项目的目录架构
mingfeilove
同问
小陈
工具安装-install, 项目创建-init
用这个工具, 自己生成一下, 自己去看下. 很完善, 每个目录各司其职., 命令是init
sudden3
刚刚用gf工具生成的项目中 没有internal\controller目录 只有handler
强仔
耐心等等,应该是新的版本还没发布,我今天下载的也是这个情况
sudden3
我刚刚在git上拉下来gf-cli的源码然后自己编译用上了哈哈
aries
现在的v2给我的感觉就是从php直接变成java了,适合多人开发,对我们小公司来说一个人负责一个项目的这咱来说,过于繁琐了
郭强
v2
更强调规范和自动化,例如命名、分层从代码以及工具层面进行了规范;通过工具对重复代码、繁琐的接口文档维护进行了自动化生成。从企业研效以及整体工程设计来讲,是很大的一次进步。小陈
我感觉这种目录结构, gf只是给个建议, 你要不要按这样, 还是干脆连目录都不要, 完全看你个人意愿.
youxue2016
建议出个视频教程吧.我是v1版本过来的,现在看到v2的目录描述文档,真是摸不到头脑啊.各个目录的功能,能否用实例的方式进行下讲解呢.
王中阳Go
可以看看这篇文章,应该对你有帮助:https://juejin.cn/post/7156119733312438279
后期我会花时间帮社区出入门视频。
钱波
dao
层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。业务逻辑、包括看似只是简单的数据操作的逻辑都应当封装到service
中,service
中包含多个业务模块,每个模块独自管理自己的dao
对象,service
与service
之间通过相互调用方法来实现数据通信而不是随意去调用其他service
模块的dao
对象。建议举一个例子给我们学习一下,以便更加深刻的理解这句话的含义!
白羽
个人拙见:
对于 dao 层的 CRUD,如果是单表查询,自然是不会出现问题。但是如果对于数据库设计的范式等级很高的情况呀,很少会出现冗余的情况,往往我们进行查询的时候会连表查询,这时就会出现问题。
为了满足业务,我们会有两种方式:
1.每条Sql语句都完成一个 service 层的业务,service层直接调用dao层结果即可,这样的数据具有特征性,当然复用性也会降低。
2.每条Sql语句尽量只完成自己的最基本的功能,service层会进一步对数据进行处理(这样就保证了dao层的代码尽量保证通用性)。
当然这里最大的问题就是业务和开发逻辑的冲突(我认为),很难去控制一整个项目都是按照上述两种规则中一种去规范开发。更多的时候我们会按照从易的开发逻辑进行。
jiftle
gf init 产生的代码里没有包含service的例子,建议增加.
刘三少
郭强 大佬请教一下,我在实际开发中,常常需要对一些第三方包进行二次封装,例如jwt、casbin、一些SDK等。
这些包有着如下特征:
问题是:这些包放到哪里比较合适??我感觉放在utility比较别扭,毕竟这些包都是专门为某个service提供的
admin888
可以把jwt,casbin放到 components 组件层,里面封装所有的组件,service层可以调用这些组件。
陈卓
感觉放utility里面挺好的,就算只为一个service服务,这也算是个第三方包,况且没准后面其他的service又会用上呢
FengY
感觉挺好的,V1在接口抽象方面设计的不好,这里就细分了Servcie和logic,把抽象和实现分离了,这样上层或者本层只需依赖service里的接口,不需要依赖实现。
这样单元测试好做一点,V1的单元测试不太好做。
dao层不分离感觉也比较对,因为dao层比较薄,方法比较固定,也不能互相依赖,所以没必要分离。
黄志强
为啥工程开发设计下面少了一个对象封装设计???
郭强
那章文档需要更新,为了防止产生误导性,先隐藏了,等更新完成再公开。
squidward
internal/packed 这个包是干啥的
李鹏飞
郭强 大佬,您好!对于您的工程目录设计,我感觉很清晰、合理;尤其是结构化设计的思想,将server传递给controller的api,以及controller调用service的input以及output都封装为结构体进行传递,这是我也比较认可的设计。
但是在实际的项目中,我感觉这会带来一个问题,就是需要有大量的结构体和转换和拷贝,很多人担心会对接口处理的性能有影响。
我经过一些思考和尝试,目前我是通过如下的方式来实现的:
就是controller调用service时,不是直接传递结构体,也就是说,对于service提供的接口,其入参和出参,不是定义为结构体,而是定义为interface,由controller中的api实现这些interface 。
controller调用service的接口时,参数传入controller中定义的api。
这样避免了一次api到service的input和output的结构体转换和拷贝操作。
不知 你以前有没有考虑过这种方案,如有更好的意见和建议,还望指导,谢谢!
admin888
是否controller,和api和service可以使用相同的model,入参出参了
shiqinfeng
你controller实现的这些interface,输出是不是就是service用到的input结构体?这样的话,和直接传指针的效果一样吧
王中阳Go
V2最新版的视频教程在这里:https://www.bilibili.com/video/BV1Ng41167fW/
大家帮忙顶一下,觉好留赞。
lucasho
工程目录真不敢苟同,给我的感觉就是下层依赖上层设计,而且使项目目录太过混乱,service层本来就是做业务细分,每个service方法功能是单一的,所有业务请求处理都是依赖service层的。
dao和model是两个完全不一样的东西,他们的边界前者是更底层的,而后者是对业务的,有时候这两个东西,开发人员搞不清楚或是在设计工程结构的时候偷懒,经常会把两者混为一谈,这是错误的,更不要说dao和model是做相同的事。
小陈
我感觉这种目录结构, gf只是给个建议, 你要不要按这样, 还是干脆连目录都不要, 还是要mvc结构, 完全看你个人意愿. 关键看你项目吧,
guoliang_gl_zhou
项目的sql文件建议放哪?
guoliang_gl_zhou
工程目录这般设计是为了解耦,非常同意这样的做法。后面可以做个工具完善以下事项:
陈生
有多年php和java工程经验,呆过多个业务团队,目前为止遇到过最好的目录结构:
1、php laravel框架工程结构
2、饿了么内部 java工程结构
├── APP-META
├── README.md
├── anubis-marketing-api # api接口定义层(java特有,打包jar给外部调用)
├── anubis-marketing-common # 工具类相关
├── anubis-marketing-core # 业务核心层(service层承载过重或者可以复用的部分放到这)
├── anubis-marketing-infra # 基础工具层(redis/es等配置引用基础类)
├── anubis-marketing-integration # 外部依赖接口包装
├── anubis-marketing-main # springboot启动程序
├── anubis-marketing-orm # model entity map repository等 数据库相关
├── anubis-marketing-service # java里api是接口定义,这里实现接口
├── anubis-marketing-test # 测试代码目录
└── pom.xml
调用关系:
api → service → core → common、infra、integration、orm
↓ → → → → → ↑
启动:
main
carson
mark
shiqinfeng
请问怎么适配微服务
风动
model层和api层数据结构差不多的情况下,model层引用api层,貌似在controller层会出现"循环引用"问题
请大佬帮忙解答。
shiqinfeng
考虑换一种工程设计,使用兼容ddd的整洁架构的目录设计,请参考B站开源的kratos的目录设计。goframe这一套是mvc的产物,同时奔着全家桶去了,把golang变得臃肿了,不过对于快速开发一些小型项目,比较方便,效率较高
郭强
是吗?
shiqinfeng
不同的项目使用不同的设计,我都实践了,都不错
郭强
在规范使用的前提下,不会存在循环引用问题的,因为
api
和model
都是结构定义,是被引用模块。Johnny
这里没太看懂,api内不可以使用model的数据,但又可以使用model下的entity吗?有哪位大佬能帮忙解释吗?谢谢~
陈卓
鄙人的个人看法是,api和model是严格分离的。api只负责请求的校验和转发,分配给特定的controller执行具体任务。api自己会根据用户前端的需要而特别定义一套适合于传递给前端的数据结构。model下的entity则是专门与数据库中的表对应的数据结构,所以api严格意义上来说是不能直接使用entity的结构来返回数据给前端的。就好比前端需要用户的name和addr,而底层数据库中,name和ID在一张表中,ID和addr又在另外一张表中,这个时候就需要api将表内容进行拆分组合,形成自己的name-addr结构体返回给前端。
Johnny
是的,我和你理解的一样,而且文档好像也是这么说的,但我看gf-demo-user和当前文档中的示例又都把entity中的数据暴露到API中直接使用了
coderon
entity和do的区别是啥?
从自动生成的代码来看,entity是有确定类型的结构体,一般用于将数据库类型查询映射到entity。
do是大都是拥有interface{} 类型的结构体。
此外使用起来的场景是啥?
cvcv
这种关于目标结构的设计的文档非常有用对于新手来说,很多人不明所以,都是模范写。如果能够明白它背后的设计,那么实现和维护项目将会轻松很多。
为什么需要这个设计(问题和需求), 它是怎么解决了什么问题以及如何解决的,它的优势和缺点
没有绝对完美的目录设计,只有适合自己的,根据自己的需求进行调整出来适合自己的目录才是最好的。
对于不同的项目可以有不同的调整,对于简单的项目,可以简化和合并目录结构,没必须采用复杂和高度封装的目录结构
小陈
是的, 简单的, 甚至一个文件就行了. 这种规范只是对项目的通用建议.
小陈
所以, 可以参考, 也可以直接用. 不需要拘泥.