GoFrame系列:4、项目结构及开发明细

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项目文档,如: 设计文档、帮助文档等等。
i18nI18N国际化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控制,以及流程变量传递。

GoHTTP请求流程中,不存在”全局变量”获取请求参数的方式,只有将上下文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

img

7.3 上下文变量注入

上下文的变量必须在请求一开始便注入到请求流程中,以便于其他方法调用,因此我们使用中间件来实现。

https://github.com/gogf/gf-demos/blob/168e049e58528413cd248c05c729840e552ece94/app/service/middleware.go#L15

img

7.4 上下文变量使用

约定俗成的,方法的第一个参数往往预留给context.Context类型参数使用,以便接受上下文变量,特别是service层的方法。例如:

https://github.com/gogf/gf-demos/blob/master/app/service/user.go

img

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层在使用的时候只会看到几个公开的业务逻辑封装对象。

img

9.2 数据校验

与客户端定义的输入接口是由api层的代码来做的校验,service层的代码仅对内部定义的参数进行必要的校验;有时往往也不需要做参数校验,认为内部调用的参数都是可信任的。例如:

注册逻辑:

img

9.3 内部引用

service内部的对象之间存在相互引用,直接使用对应的变量即可,例如:

img

9.4 实现代码

https://github.com/gogf/gf-demos/blob/master/app/service/user.go

10. 数据文件创建

10.1 代码生成

该模型通过GF工具链的gf gen dao命令生成,提供了强大灵活的数据表操作方式。生成的数据文件:

img

生成的文件介绍请参考 gen 代码生成 章节。

10.2 dao使用

通过dao包引用对应的对象即可,dao往往是被service代码层调用。

查找dao中的User对象:

img

调用User对象的Save方法保存数据到数据表中。

img

10.3 model使用

通过model包引用对应的数据结构定义即可。

查找model中使用到api的请求数据结构ApiUserSignInReq

img

通过ApiUserSignInReq接受请求参数:

img

11. Swagger生成

如果对使用swagger接口文档比较感兴趣可以参考此章节介绍,不需要的话可以忽略该章节。

swagger接口文档主要用于前后端的接口定义。Golangswagger文档通过注释的形式编写到api层的代码中,使得接口文档可以随着代码一起维护,降低代码与文档不一致的风险,并通过gf-cli工具生成:swagger API文档生成

11.1 swagger编写

swagger的语法请参考第三方仓库swag:https://github.com/swaggo/swag

目前仅此一家Golang Swagger组件库,Golangswagger编写体验并不是特别友好,聊胜于无吧。

img

11.2 swagger生成

我们这里使用以下命令生成:

gf swagger --pack

其中gf swagger命令解析并生成swagger.json文档到项目根目录的swagger路径下,同时这里的--pack选项将swagger.json打包为Golang代码文件生成到项目根目录的packed路径下。

img

具体请参考 swagger API文档生成 章节。

11.3 swagger插件

我们这里使用到了GoFrameswagger插件:https://github.com/gogf/swagger

按照仓库介绍说明,我们在boot启动设置模块中添加插件的注册:

img

11.4 swagger文档查看

随后可以启动程序访问查看swagger接口文档页面:http://127.0.0.1:8199/swagger

img

12. 基础类库存放

主要固定返回数据格式及数据结构。

其中JsonExitJson的区别在于,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. 最后

基本涉及开发的就这些东西,下一步我们看一下项目配置、路由服务以及运行方式。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昵称系统有问题

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值