GoFrame系列:4、项目结构及开发明细
文章目录
1. 前言
上节我们已经安装demo并测试搭建该框架并运行的demo,也发现该框架搭建web后台是比较方便的,接下来我们了解应用的目录结构并了解demo是如何进行开发的,以此来看一下我们后续自己开发web如何在该框架中展开。
从业务开发上基本分为四大部分:api、dao、model、service
2. 项目结构
如果是Package
源码包项目,开发者可随意定义目录结构。
如果是业务类型项目,GoFrame
官方推荐的Go
项目目录结构如下:
/
├── app
│ ├── api
│ ├── dao
│ ├── model
│ └── service
├── boot
├── config
├── docker
├── document
├── i18n
├── library
├── packed
├── public
├── router
├── template
├── Dockerfile
├── go.mod
└── main.go
目录/文件名称 | 说明 | 描述 |
---|---|---|
app | 业务逻辑层 | 所有的业务逻辑存放目录。 |
- api | 业务接口 | 接收/解析用户输入参数的入口/接口层。 |
- dao | 数据访问 | 数据库的访问操作,仅包含最基础的数据库CURD 方法 |
- model | 结构模型 | 数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义 |
- service | 逻辑封装 | 业务逻辑封装层,实现特定的业务需求,可供不同的包调用。 |
boot | 初始化包 | 用于项目初始化参数设置,往往作为main.go 中第一个被import 的包。 |
config | 配置管理 | 所有的配置文件存放目录。 |
docker | 镜像文件 | Docker 镜像相关依赖文件,脚本文件等等。 |
document | 项目文档 | Documentation项目文档,如: 设计文档、帮助文档等等。 |
i18n | I18N国际化 | I18N国际化配置文件目录。 |
library | 公共库包 | 公共的功能封装包,往往不包含业务需求实现。 |
packed | 打包目录 | 将资源文件打包的Go 文件存放在这里,boot 包初始化时会自动调用。 |
public | 静态目录 | 仅有该目录下的文件才能对外提供静态服务访问。 |
router | 路由注册 | 用于路由统一的注册管理。 |
template | 模板文件 | MVC 模板文件存放的目录。 |
Dockerfile | 镜像描述 | 云原生时代用于编译生成Docker镜像的描述文件。 |
go.mod | 依赖管理 | 使用Go Module 包管理的依赖描述文件。 |
main.go | 入口文件 | 程序入口文件。 |
在实践中,小伙伴们可以根据实际情况增删目录。
注意:如果需要提供静态服务,那么所有静态文件都需要存放到public
目录下,仅有该目录下的静态文件才能被外部直接访问。不推荐将程序当前运行目录加入到静态服务中。
项目创建推荐使用GF
工具链gf init
命令,具体请参考 开发工具 章节。
3. 分层设计
GoFrame
官方推荐的代码分层设计。
3.1 控制器
控制器负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用service
实现业务逻辑处理。
控制器代码位于/app/api
。
3.2 业务逻辑
业务逻辑是需要封装的,特别是一些可复用的业务逻辑,并被控制器调用实现业务逻辑处理。
逻辑封装的代码位于/app/service
。
3.3 数据访问
数据访问代码层负责所有的数据集合(数据表)访问收口,将数据集合按照面向对象的方式进行封装。
数据访问的代码位于/app/dao
。
3.4 模型定义
模型定义代码层负责维护所有的数据结构定义,包括所有的输入输出数据结构定义。
模型定义代码层中仅包含数据结构定义,不包含任何的方法定义。
模型定义的代码位于/app/model
。
3.5 模板解析
模板解析是可选的,在实践中往往可以采用MVVM
的模式,例如使用vue
/react
等框架实现模板解析。如果使用经典的模板解析,可以通过GoFrame
框架强大的模板引擎实现模板解析。
模板文件的存放于/template
。
4. 数据库设计
我们创建一个简单的用户表来做演示。
https://github.com/gogf/gf-demos/blob/master/document/sql/create.sql
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`passport` varchar(45) NOT NULL COMMENT '用户账号',
`password` varchar(45) NOT NULL COMMENT '用户密码',
`nickname` varchar(45) NOT NULL COMMENT '用户昵称',
`create_at` datetime DEFAULT NULL COMMENT '创建时间',
`update_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
为简化示例项目的接口实现复杂度,这里的password
没有做任何加密处理,明文存放密码数据。
5. 包名设计
5.1 包名约定
根据官方《Effective Go》建议,包名尽量采用言简意赅的名称(short, concise, evocative
)。并且推荐通过不同的import
路径来区分相同包名的包引入。
5.2 包名设计
如果使用GF
开发业务项目,那么基本可以不用考虑包名设计的问题,因为从v1.15
版本开始,GF
官方推荐使用面向对象的封装方式,项目中app
目录下往往只存在api
, dao
, model
, service
四个包名,每个包内部通过对象的形式来封装具体的数据模型或者业务逻辑。
6. 控制器实现
6.1 结构化约束
控制器的输入与输出使用了结构体定义进行约束,结构化维护输入输出数据结构是推荐的方式。例如:
// 账号唯一性检测请求参数,用于前后端交互参数格式约定
type UserApiCheckPassportReq struct {
Passport string `v:"required#账号不能为空"`
}
虽然只有一个参数,也采用了结构化定义,我们直接查看该结构体便可得知该接口的输入参数格式,而不用进入代码中去分析,从而极大提高维护效率。
6.2 结构体转换
结构体转换可以使用GetStruct
或者Parse
方法,其中Parse
同时可以执行数据校验。结构体转换方法的参数都可以给定一个结构体的空指针,内部会自动初始化结构体对象,转换失败(例如提交参数不存在)不会执行初始化。例如:
var (
data *model.UserApiSignInReq
)
if err := r.Parse(&data); err != nil {
response.JsonExit(r, 1, err.Error())
}
6.3 数据校验
客户端提交的数据都是不可信的,必须要做数据校验。
可以通过给结构体绑定v
的标签进行设定校验规则以及定义的错误提示。例如:
// 登录请求参数,用于前后端交互参数格式约定
type UserApiSignInReq struct {
Passport string `v:"required#账号不能为空"`
Password string `v:"required#密码不能为空"`
}
6.4 数据传参
控制器负责接收、转换、校验、处理请求参数后,将所需的参数传递给调用的service
对象方法,而不是直接将Request
对象传递给service
。例如:
func (a *apiUser) SignIn(r *ghttp.Request) {
var (
data *model.UserApiSignInReq
)
if err := r.Parse(&data); err != nil {
response.JsonExit(r, 1, err.Error())
}
if err := service.User.SignIn(r.Context(), data.Passport, data.Password); err != nil {
response.JsonExit(r, 1, err.Error())
} else {
response.JsonExit(r, 0, "ok")
}
}
6.4 实现代码
https://github.com/gogf/gf-demos/blob/master/app/api/user.go
7. 上下文变量
上下文变量指的是标准库的context.Context
,是一个接口对象。主要用于goroutine
的异步IO控制,以及流程变量传递。
在Go
的HTTP
请求流程中,不存在”全局变量”获取请求参数的方式,只有将上下文context
变量传递到后续流程的方法中,而context
上下文变量即包含了所有需要传递的共享变量。并且该context
中的共享变量应当是事先约定的,并且往往存储为对象指针形式。
7.1 结构定义
在该示例中,我们的上下文变量的数据结构定义为:
https://github.com/gogf/gf-demos/blob/master/app/model/context.go
// 请求上下文结构
type Context struct {
Session *ghttp.Session // 当前Session管理对象
User *ContextUser // 上下文用户信息
}
// 请求上下文中的用户信息
type ContextUser struct {
Id uint // 用户ID
Passport string // 用户账号
Nickname string // 用户名称
}
7.2 逻辑封装
由于该上下文对象也是和业务逻辑相关的,因此我们需要通过service
对象将上下文变量封装起来再供其他模块使用。
https://github.com/gogf/gf-demos/blob/master/app/service/context.go
7.3 上下文变量注入
上下文的变量必须在请求一开始便注入到请求流程中,以便于其他方法调用,因此我们使用中间件来实现。
https://github.com/gogf/gf-demos/blob/168e049e58528413cd248c05c729840e552ece94/app/service/middleware.go#L15
7.4 上下文变量使用
约定俗成的,方法的第一个参数往往预留给context.Context
类型参数使用,以便接受上下文变量,特别是service
层的方法。例如:
https://github.com/gogf/gf-demos/blob/master/app/service/user.go
8. 中间件使用
8.1 跨域处理
允许跨域请求。(关于跨域:https://segmentfault.com/a/1190000015597029)
// 允许接口跨域请求
func (s *serviceMiddleware) CORS(r *ghttp.Request) {
r.Response.CORSDefault()
r.Middleware.Next()
}
8.2 鉴权处理
只有在用户登录后才可通过。
// 鉴权中间件,只有登录成功之后才能通过
func (s *serviceMiddleware) Auth(r *ghttp.Request) {
if User.IsSignedIn(r.Context()) {
r.Middleware.Next()
} else {
r.Response.WriteStatus(http.StatusForbidden)
}
}
8.3 上下文注入
前一章节已介绍过。
9. 业务逻辑封装
9.1 职责划分
所有的业务逻辑实现均封装于service
层中,不推荐实现于控制器api
中。service
层的包名只有一个,通过面向对象的方式进行封装。api
层在使用的时候只会看到几个公开的业务逻辑封装对象。
9.2 数据校验
与客户端定义的输入接口是由api
层的代码来做的校验,service
层的代码仅对内部定义的参数进行必要的校验;有时往往也不需要做参数校验,认为内部调用的参数都是可信任的。例如:
注册逻辑:
9.3 内部引用
在service
内部的对象之间存在相互引用,直接使用对应的变量即可,例如:
9.4 实现代码
https://github.com/gogf/gf-demos/blob/master/app/service/user.go
10. 数据文件创建
10.1 代码生成
该模型通过GF
工具链的gf gen dao
命令生成,提供了强大灵活的数据表操作方式。生成的数据文件:
生成的文件介绍请参考 gen 代码生成 章节。
10.2 dao
使用
通过dao
包引用对应的对象即可,dao
往往是被service
代码层调用。
查找dao
中的User
对象:
调用User
对象的Save
方法保存数据到数据表中。
10.3 model
使用
通过model
包引用对应的数据结构定义即可。
查找model
中使用到api
的请求数据结构ApiUserSignInReq
:
通过ApiUserSignInReq
接受请求参数:
11. Swagger生成
如果对使用swagger
接口文档比较感兴趣可以参考此章节介绍,不需要的话可以忽略该章节。
swagger
接口文档主要用于前后端的接口定义。Golang
的swagger
文档通过注释的形式编写到api
层的代码中,使得接口文档可以随着代码一起维护,降低代码与文档不一致的风险,并通过gf-cli
工具生成:swagger API文档生成。
11.1 swagger
编写
swagger
的语法请参考第三方仓库swag
:https://github.com/swaggo/swag
目前仅此一家
Golang Swagger
组件库,Golang
的swagger
编写体验并不是特别友好,聊胜于无吧。
11.2 swagger
生成
我们这里使用以下命令生成:
gf swagger --pack
其中gf swagger
命令解析并生成swagger.json
文档到项目根目录的swagger
路径下,同时这里的--pack
选项将swagger.json
打包为Golang
代码文件生成到项目根目录的packed
路径下。
具体请参考 swagger API文档生成 章节。
11.3 swagger
插件
我们这里使用到了GoFrame
的swagger
插件:https://github.com/gogf/swagger
按照仓库介绍说明,我们在boot
启动设置模块中添加插件的注册:
11.4 swagger
文档查看
随后可以启动程序访问查看swagger
接口文档页面:http://127.0.0.1:8199/swagger
12. 基础类库存放
主要固定返回数据格式及数据结构。
其中JsonExit
与Json
的区别在于,JsonExit
调用时会输出JSON
数据后直接退出当前的路由方法;而Json
在执行输出后会继续执行后续的路由方法逻辑。
https://github.com/gogf/gf-demos/blob/master/library/response/response.go
package response
import (
"github.com/gogf/gf/net/ghttp"
)
// 数据返回通用JSON数据结构
type JsonResponse struct {
Code int `json:"code"` // 错误码((0:成功, 1:失败, >1:错误码))
Message string `json:"message"` // 提示信息
Data interface{} `json:"data"` // 返回数据(业务接口定义具体数据结构)
}
// 标准返回结果数据结构封装。
func Json(r *ghttp.Request, code int, message string, data ...interface{}) {
responseData := interface{}(nil)
if len(data) > 0 {
responseData = data[0]
}
r.Response.WriteJson(JsonResponse{
Code: code,
Message: message,
Data: responseData,
})
}
// 返回JSON数据并退出当前HTTP执行函数。
func JsonExit(r *ghttp.Request, err int, msg string, data ...interface{}) {
Json(r, err, msg, data...)
r.Exit()
}
13. 最后
基本涉及开发的就这些东西,下一步我们看一下项目配置、路由服务以及运行方式。