【ginny 系列】 基于Go的web后台开发,工具函数、错误处理还有中间件

工具函数、错误处理还有中间件

现在你会发现自己可以轻松自由地写出大量的接口:如果你想的话,甚至可以一天写上百个接口!

但是在这样野蛮的构建你的应用的时候,你总会觉得有点不适:要写大量的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 handler

import (
    "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.go

import "github.com/ShiinaOrez/ginny/handler"

func Register(c *gin.Context) {
    var data RegisterPayload
    if 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.go

package errno

import "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.go

package errno

var (
    // 由于各种原因导致的Bad Request
    PayloadBadRequest = &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.go

func 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 RegisterPayload
    if 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 []HandlerFunc

func (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 / 2

func (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) {
    // pre
    c.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).")
}

这里就只是简单的介绍一下中间件,具体的中间件会在后面进行撰写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小夕Coding

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

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

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

打赏作者

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

抵扣说明:

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

余额充值