【ginny 系列】 基于Go的web后台开发,工具函数、错误处理还有中间件
文章目录
- 工具函数、错误处理还有中间件
- 4.1 工具函数:重用你的代码片段
- 4.1.1 gin生成响应的方式
- 4.2 错误处理
- 4.3 中间件
- 4.3.1 HandlerFunc
- 4.3.2 HandlersChain
工具函数、错误处理还有中间件
现在你会发现自己可以轻松自由地写出大量的接口:如果你想的话,甚至可以一天写上百个接口!
但是在这样野蛮的构建你的应用的时候,你总会觉得有点不适:要写大量的c.JSON()
和gin.H{}
,在每次的开头几乎都会check一下BadRequest
的情况,虽然实际上是一种错误,但是我们只是返回了字符串而已。
接下来的几个小节,我们将会再次小小的重构
我们的代码。
4.1 工具函数:重用你的代码片段
首先我们要知道我们的第一个目的是什么:让我们不用再在handler
中写c.JSON()
!
那我们在撰写工具函数之前,我们应该更加多的去了解c.JSON()
方法和gin.H
类型:
4.1.1 gin生成响应的方式
让我们打开gin.Context
所在的文件, 我们找到了这样几个方法:
func (c *Context) JSON(code int, obj interface{}) {}func (c *Context) Status(code int) {}func (c *Context) Abort() {}
func (c *Context) AbortWithStatus(code int) {}
func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {}
func (c *Context) AbortWithError(code int, err error) *Error {}
一般来说,响应分为两部分:一是状态码,二是主体信息,状态码用来简单的标识响应的信息:比如:
- 200 OK
- 400 Bad Request
- 401 Unauthorized
- 404 Not Found
- 405 Method Not Allowed
就像上面这样,我们可以通过一个响应的状态码来轻松的对其进行分类,返回的信息就包含在返回的body里面。
context.JSON()
方法接受状态码和一个任意类型的object
,然后内部调用context.Render()
方法:首先使用context.Status()
方法来产生响应的状态码,然后再处理body部分。
所以看其他的方法,context.Abort()
方法是用来中断对于剩余编程部件
的使用的方法。至于什么是编程部件,我会在中间件的部分去讲解。但是要注意!abort和返回一个响应完全是两个概念!
由此来看,AbortWithStatus()
, AbortWithStatusJSON()
和AbortWithError()
其实都是由以上几个方法拼接而来的。
我们返回的响应无非包括以下两个大类:预料内的和预料外的,所谓预料内的就是指我们知道发生错误的详细情况和类别,或者是根本没有错误。而预料外的错误,则是由更加底层的接口产生的错误来提供详细说明情况的。
所以我们可以在./handler/handler.go
中封装我们自己的工具函数:
// ./handler/handler.go
package handlerimport ("net/http""github.com/gin-gonic/gin"
)func SendResponse(c *gin.Context, data interface{}) {c.JSON(http.StatusOK, data)
}func SendUnauthorized(c *gin.Context) {c.AbortWithStatus(http.StatusUnauthorized)
}func SendBadRequest(c *gin.Context) {c.AbortWithStatus(http.StatusBadRequest)
}func SendNotFound(c *gin.Context) {c.AbortWithStatus(http.StatusNotFound)
}
有了这些函数,我们就可以这样去重写我们的handler:
// ./handler/register/register.goimport "github.com/ShiinaOrez/ginny/handler"func Register(c *gin.Context) {var data RegisterPayloadif err := c.BindJSON(&data); err != nil {handler.SendBadRequest(c)return}if model.CheckUserByUsername(data.Username) {handler.SendUnauthorized(c)return}model.CreateUser(data.Username, data.Password)handler.SendResponse(c, "Successful!")return
}
显然我们代码的可读性更强了,我们的代码也可以变得更加精简。但是也有问题暴露出来:比如产生NotFound
错误的方式不止一种,可能是UserNotFound
,也有可能是BlogNotFound
,我们的数据库中有多少种实体,就有多少种NotFound,而我们只使用一个SendNotFound()
方法的话,就是以一概全,这是不对的。所以在下面我们会进行下一个主题:错误处理。
提示:可以在本教程的附属仓库 https://github.com/ShiinaOrez/ginny.git 中通过标签``v0.2.2``来查看这个示例。
4.2 错误处理
我们在实现一段业务逻辑的时候,经常会遇到各种复杂场景的错误,因此定义特殊的错误类型是很有必要的。一是方便我们自定义错误,二是有利于错误信息的传递。然后让我们来自定义一个自己的错误类型:
type Error struct {ErrorCode string `json:"error_code"`Message string `json:"message"`
}
Error.ErrorCode
字段是用于唯一标识一个具体错误的字符串。而Error.Message
是对于该错误的具体描述。
比如我们可以声明一个Bad Request
的错误:
var (ErrorBadRequest = &Error{ErrorCode: "0001",Message: "Bad Request!",}
)
这样做的话,我们就可以自定义一些错误,然后在产生具体错误的时候返回之。(毕竟参数是interface{}
类型嘛)比如我们再次改造一下我们的register.go
现在的目录结构:
.
├── handler
│ ├── handler.go
│ ├── ...
│ └── register
│ └── register.go
├── model
│ ├── init.go
│ └── user.go
├── pkg
│ └── errno
│ ├── errno.go // 声明了错误类型和相应的方法
│ └── code.go // 存放错误常量
├── router
│ └── router.go
└── main.go
// ./pkg/errno/errno.gopackage errnoimport "fmt"type Error struct {ErrorCode string `json:"error_code"`Message string `json:"message"`
}func (err *Error) Error() string { // 满足内置的error接口return fmt.Sprintf("Error(%s): %s.", err.ErrorCode, err.Message)
}
// ./pkg/errno/code.gopackage errnovar (// 由于各种原因导致的Bad RequestPayloadBadRequest = &Error{ErrorCode: "00001", Message: "Bad Request: lack of payload."}ParamBadRequest = &Error{ErrorCode: "00002", Message: "Bad Request: lack of parameters."}// User类型相关的错误UserNotFound = &Error{ErrorCode: "10001", Message: "DB: User Not Found!"}UserAleadyExisted = &Error{ErrorCode: "10002", Message: "Register: User already existed!"}
)
// ./handler/handler.gofunc SendUnauthorized(c *gin.Context, err error) {c.AbortWithStatusJSON(http.StatusUnauthorized, err)c.Error(err)
}func SendBadRequest(c *gin.Context, err error) {c.AbortWithStatusJSON(http.StatusBadRequest, err)c.Error(err)
}func SendNotFound(c *gin.Context, err error) {c.AbortWithStatusJSON(http.StatusNotFound, err)c.Error(err)
}func SendError(c *gin.Context, err error) {c.AbortWithStatusJSON(500, err)c.Error(err)
}
import "github.com/ShiinaOrez/ginny/pkg/errno"func Register(c *gin.Context) {var data RegisterPayloadif err := c.BindJSON(&data); err != nil {handler.SendBadRequest(c, errno.PayloadBadRequest)return}if model.CheckUserByUsername(data.Username) {handler.SendError(c, errno.UserAleadyExisted)return}model.CreateUser(data.Username, data.Password)handler.SendResponse(c, "Successful!")return
}
这样就完成了我们的自定义错误。而且还在日志中有错误输出。
提示:可以在本教程的附属仓库 https://github.com/ShiinaOrez/ginny.git 中通过标签``v0.2.3``来查看这个示例。
4.3 中间件
在开始讲解中间件之前,我们要先理一下gin
中的内部类型逻辑:
4.3.1 HandlerFunc
type HandlerFunc func(*Context)
也就是说任何只接受gin.Context
为参数的函数就可以是gin
眼中的业务逻辑处理函数了。而我们要讲解的中间件也是一个HandlerFunc
4.3.2 HandlersChain
type HandlersChain []HandlerFuncfunc (c HandlersChain) Last() HandlerFunc {if length := len(c); length > 0 {return c[length-1]}return nil
}
我们可以看到一个在我们眼中几乎不怎么出现的类型:HandlersChain
,直接翻译的话很明显可以看出,是一个由HandlerFunc
串成的链子。
在这时你应该已经焕然大悟了,原来一个接受到了一个请求之后,它是会经历一系列的HandlerFunc
的:
Handler-1 Handler-2
*Context |---------| |---------|
request ---> ---> ---> ...|---------| |---------|
之前特意提起过的Context.Abort()
方法,还有一个对应的Context.Next()
方法。Abort
方法用于中断这个HandlersChain
的执行,但是还需要特意的返回,这是因为Abort()
方法内部其实只是设置了Context.index
属性:
const abortIndex int8 = math.MaxInt8 / 2func (c *Context) Abort() {c.index = abortIndex
}
Context
只是通过index
属性来判断HandlersChain
是否被abort了:
func (c *Context) IsAborted() bool {return c.index >= abortIndex
}
我们假设我们现在处于HandlersChain
的[x]
位置,那么我们下一个就应该执行HandlersChain[x+1]
的HandlerFunc,这个时候就要用到Context.Next()
方法:
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}
}
但是这里我们可以看到,如果是每次都要调用Context.Next()
方法,这个方法应该这么写:
func (c *Context) Next() {c.index++c.handlers[c.index](c)
}
但是源码中使用了一个循环,这就代表着一旦我们使用Context.Next()
启动了这个HandlersChain
,它就会自动执行下去,除非中间调用了Context.Abort()
方法。
也就是说:如果你保证一个中间件完全是在中间执行的,那么你完全可以不用写c.Next()
虽说是HandlersChain,但是其实也不是一定按照链子的方式来执行,事实上除了前期处理工作,也可以有后续处理:
func MyMiddleware(c *gin.Context) {// prec.Next()// after
}
比如我们可以写一个自己的计算响应时间的中间件:(虽然gin自带有很好的log,但是就当做练习来写)
import ("fmt""time""github.com/gin-gonic/gin"
)func Timer(c *gin.Context) {startTime := time.Now()c.Next()endTime := time.Now()fmt.Println("Cost:", endTime.Sub(startTime).Nanoseconds(), "(nano seconds).")
}
这里就只是简单的介绍一下中间件,具体的中间件会在后面进行撰写。
【ginny 系列】 基于Go的web后台开发,工具函数、错误处理还有中间件相关推荐
- 基于Token的WEB后台认证机制
转载自:基于Token的WEB后台认证机制 几种常用的认证机制 HTTP Basic Auth HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和passwor ...
- 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(上)
目录 前言 第1章 安装 第2章 程序的基本结构 第3章 模板 第4章 Web表单 第5章 数据库 第6章 电子邮件 第7章 大型程序的结构 前言 学习Python也有一个半月时间了,学到现在感觉还是 ...
- 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(下)
目录 前言 第8章 用户认证 第9章 用户角色 第10章 用户资料 第11章 博客文章 第12章 关注者 第13章 用户评论 第14章 应用编程接口 前言 第1章-第7章学习实践记录请参见:< ...
- Flask Web开发:基于Python的Web应用开发实战
<Flask Web开发:基于Python的Web应用开发实战> 虽然简单的网站(Flask+Python+SAE)已经上线,但只是入门.开发大型网站,系统地学习一遍还是有必要的. 201 ...
- 学习《Flask Web开发:基于Python的Web应用开发实战》分享
学习<Flask Web开发:基于Python的Web应用开发实战>分享一直在说学习Python,对同事,对朋友,都说我正在学习Python,这无形给自己一定的压力,促使自己要去学习,进步 ...
- 《FlaskWeb开发:基于Python的Web应用开发实战》笔记
开源库的cdn加速 可以在这里直接搜索复制script链接 https://www.bootcdn.cn/ requirements.txt文件的生成与使用 生成requirements文件:$ pi ...
- 基于浏览器的开源“管理+开发”工具,Pivotal MySQL*Web正式上线!
基于浏览器的开源"管理+开发"工具,Pivotal MySQL*Web正式上线! https://www.sohu.com/a/168292858_747818 https://g ...
- 《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)
内容提要 在学习"狗书"<Flask Web开发:基于Python的Web应用开发实战>的过程中,一直遇到各种各样的坑.该书的第一部分是"Flask简介&qu ...
- 前端系列——vue2+高德地图web端开发(poi搜索两种方式)
前端系列--vue2+高德地图web端开发(poi搜索) 前言 基础 什么是poi搜索 1. 输入提示结合poi搜索 官方代码 步骤 1.进行plugins插件注册 2.data中编写placeSea ...
最新文章
- 19.04.02笔记
- 2020年最具潜力44个顶级开源项目,涵盖11类 AI 学习框架、平台
- 荣获计算机视觉“奥斯卡”奖提名的年轻人!康奈尔大四学生林之秋的科研之道...
- SQL SERVER中直接循环写入数据
- postgresql 备份恢复(一)
- Linux VNC黑屏(转)
- PANEL中显示窗体
- Anaconda管理多版本的python环境
- DevTools failed to load source map: Could not load content for…System error: net::ERR_FILE_NOT_FOUN
- python2.7.5 怎么装redis_python中Redis的简要介绍以及Redis的安装,配置
- (转载)C#中如何获取当前路径的几种方法
- 虹膜识别应用多样化 6亿美元市场待挖掘
- SVN server安装步骤
- SQLServer数据库文件压缩
- 收藏 | 史上最详细的 Landsat 1-9 系列数据集介绍~
- Unity:Firebase接入Apple登录
- 嵌入式设备的机器码、cpu的id号以及网卡mac地址
- 国家天地图API 创建点 覆盖物
- clickhouse建表语句行数太多导致报错 Unmatched parentheses: (
- ESP32 NVS同windows文件系统的类比,附上一段NVS操作的代码解析