文章目录

  • 工具函数、错误处理还有中间件
    • 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后台开发,工具函数、错误处理还有中间件相关推荐

  1. 基于Token的WEB后台认证机制

    转载自:基于Token的WEB后台认证机制 几种常用的认证机制 HTTP Basic Auth HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和passwor ...

  2. 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(上)

    目录 前言 第1章 安装 第2章 程序的基本结构 第3章 模板 第4章 Web表单 第5章 数据库 第6章 电子邮件 第7章 大型程序的结构 前言 学习Python也有一个半月时间了,学到现在感觉还是 ...

  3. 《Flask Web开发——基于Python的Web应用开发实践》一字一句上机实践(下)

    目录 前言 第8章 用户认证 第9章 用户角色 第10章 用户资料 第11章 博客文章 第12章 关注者 第13章 用户评论 第14章 应用编程接口   前言 第1章-第7章学习实践记录请参见:< ...

  4. Flask Web开发:基于Python的Web应用开发实战

    <Flask Web开发:基于Python的Web应用开发实战> 虽然简单的网站(Flask+Python+SAE)已经上线,但只是入门.开发大型网站,系统地学习一遍还是有必要的. 201 ...

  5. 学习《Flask Web开发:基于Python的Web应用开发实战》分享

    学习<Flask Web开发:基于Python的Web应用开发实战>分享一直在说学习Python,对同事,对朋友,都说我正在学习Python,这无形给自己一定的压力,促使自己要去学习,进步 ...

  6. 《FlaskWeb开发:基于Python的Web应用开发实战》笔记

    开源库的cdn加速 可以在这里直接搜索复制script链接 https://www.bootcdn.cn/ requirements.txt文件的生成与使用 生成requirements文件:$ pi ...

  7. 基于浏览器的开源“管理+开发”工具,Pivotal MySQL*Web正式上线!

    基于浏览器的开源"管理+开发"工具,Pivotal MySQL*Web正式上线! https://www.sohu.com/a/168292858_747818 https://g ...

  8. 《Flask Web开发:基于Python的Web应用开发实战》笔记(原创)

    内容提要 在学习"狗书"<Flask Web开发:基于Python的Web应用开发实战>的过程中,一直遇到各种各样的坑.该书的第一部分是"Flask简介&qu ...

  9. 前端系列——vue2+高德地图web端开发(poi搜索两种方式)

    前端系列--vue2+高德地图web端开发(poi搜索) 前言 基础 什么是poi搜索 1. 输入提示结合poi搜索 官方代码 步骤 1.进行plugins插件注册 2.data中编写placeSea ...

最新文章

  1. 19.04.02笔记
  2. 2020年最具潜力44个顶级开源项目,涵盖11类 AI 学习框架、平台
  3. 荣获计算机视觉“奥斯卡”奖提名的年轻人!康奈尔大四学生林之秋的科研之道...
  4. SQL SERVER中直接循环写入数据
  5. postgresql 备份恢复(一)
  6. Linux VNC黑屏(转)
  7. PANEL中显示窗体
  8. Anaconda管理多版本的python环境
  9. DevTools failed to load source map: Could not load content for…System error: net::ERR_FILE_NOT_FOUN
  10. python2.7.5 怎么装redis_python中Redis的简要介绍以及Redis的安装,配置
  11. (转载)C#中如何获取当前路径的几种方法
  12. 虹膜识别应用多样化 6亿美元市场待挖掘
  13. SVN server安装步骤
  14. SQLServer数据库文件压缩
  15. 收藏 | 史上最详细的 Landsat 1-9 系列数据集介绍~
  16. Unity:Firebase接入Apple登录
  17. 嵌入式设备的机器码、cpu的id号以及网卡mac地址
  18. 国家天地图API 创建点 覆盖物
  19. clickhouse建表语句行数太多导致报错 Unmatched parentheses: (
  20. ESP32 NVS同windows文件系统的类比,附上一段NVS操作的代码解析

热门文章

  1. Vue3和Vue2对比,我们如何选用?
  2. js身份证验证完整版
  3. Programming Languages PartA Week5学习笔记——SML进阶与编程哲学
  4. 铁打的java_铁打的Java,流水的编程语言,后来居上的Python
  5. NLP-Beginner 任务二:基于深度学习的文本分类+pytorch(超详细!!)
  6. 小而全的Pandas数据分析案例
  7. Clojure学习1---基础语法
  8. Laravel 社会化登录之微信网页授权登录
  9. cesium自定义远、近天空盒图片
  10. WINDOWS MOBILE获取运营商名称