Skip to content

中间件

用在 客户端服务端 之间的插件,我们称之为 中间件

中间件示意图

中间件是一个概念,其实质就是一些遵循一定规范的函数,也称做 钩子函数(Hook)

中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

定义中间件

定义一个中间件,其实就是定义一个 gin.HandlerFunc

这个 HandlerFunc 其实是一个函数,例如前面我们已经写了很多次的 GET请求、POST请求,第一个参数为路由路径,第二个参数写的就是这种格式的处理函数。

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes

对于这个 HandlerFunc,源码中是这样定义的:

github.com/gin-gonic/gin/gin.go
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

形参列表只有一个 *gin.Context 类型的参数,没有返回值。

这样的函数就是一个 HandlerFunc,就可以作为一个中间件。

eg:定义一个计时中间件

1
2
3
4
5
6
7
func timer(c *gin.Context) {
    start := time.Now()
    c.Next()
    end   := time.Since(start)

    fmt.Println(end)
}
简简单单,上面的栗子就已经定义了一个名为 timer 的中间件,用于计时。

这里的 c.Next() 是主动调用后面的 HandlerFunc,它还有一个兄弟:c.Abort(),这个我们放在后面讲。

局部注册中间件

使用中间件也很简单,我们在接收请求的时候,即编写 Handler()、或者 GET() 这些方法时,最后一个参数就可以传入一个或多个 HandlerFunc

eg:

func sleep(c *gin.Context) {
    start := time.Now()
    time.Sleep(time.Second)    // 睡眠一秒
    end   := time.Since(start)

    fmt.Println("sleep middleware: ", end)
}

func indexHandler(c *gin.Context) {
    fmt.Println("Index handler")
    c.JSON(http.StatusOK, gin.H{ "msg": "Here is index." })
}

func main() {
    r := gin.Default()

    r.GET("/", sleep, indexHandler)    // 局部注册中间件到此路由

    r.Run(":9090")
}
输出:

上面的栗子中可以看出,局部注册就是把中间件写在需要使用该中间件的路由中。

这里 timerindexHandler 本质上都是一样的,但是被调用顺序不同。

客户端请求 / 这个路径时,会先调用 timer 这个中间件,等它执行完后再去执行 indexHandler

全局注册中间件

全局注册中间件,会使得该作用域下所有路由都会执行对应的中间件。注册时使用Use() 方法。

啥意思呢?

就是说,假设你定义了 r := gin.Default()

然后你写 r.Use(timer),这样你的网站所有的路由都会执行 timer 这个中间件。

如果你是用 userG := r.Group("/user"),然后写 userG.Use(timer),这样 userG 这个路由组下所有路由都会执行 timer 这个中间件

而组外的其他路由并不一定会执行 timer。

Example

// 定义一个中间件
func timer(c *gin.Context) {
    start := time.Now()
    c.Next()
    end := time.Since(start)
    fmt.Println(end)
}

func main() {
    r := gin.Default()

    userG := r.Group("/user")    // 定义一个路由组 userG
    userG.Use(timer)             // 绑定 timer 中间件
    {
        // 以下两个路由会执行 timer 中间件
        userG.POST("/register", Register)
        userG.POST("/login", Login)
    }

    // 定义一个路由组 boogG
    bookG := r.Group("/book")
    {
        // 以下两个路由不会执行 timer 中间件
        bookG.GET("/search/:id", SearchBook)
        bookG.DELETE("/remove/:id", RemoveBook)
    }

    r.Run(":9090")
}
...

可以清楚看到,请求 /user 组下的路径时,调用了 timer 打印了时间,而请求 /book 组时没有。

中间链(职责链模式)

c.Next()

上面的栗子中,我们使用到了 c.Next() 函数,这会改变程序的执行流。

中间件里没有使用到 c.Next() 的时候,其执行流如下

没有 c.Next() 的时候会执行完前一个中间件,就执行下一个中间件。

而在中间件中使用 c.Next() 时则会改变其执行流。

其实就是将下一个中间件的执行时机提前了,但是下一个中间件执行完以后还会回到当前的中间件。

其执行流如下图:

Example
// 中间件1
func m1(c *gin.Context) {
    fmt.Println("m1 in...")
    c.Next()
    fmt.Println("m1 out...")
}

// 中间件2
func m2(c *gin.Context) {
    fmt.Println("m2 in...")
    c.Next()
    fmt.Println("m2 out...")
}

// 首页处理函数
func indexHandler(c *gin.Context) {
    fmt.Println("Index handler in...")

    c.JSON(http.StatusOK, gin.H{
        "msg": "Here is index.",
    })
    c.Next()
    fmt.Println("Index handler out...")
}


func main() {
    r := gin.Default()

    r.GET("/", m1, m2, indexHandler)    // 依次局部注册三个中间件

    r.Run(":9090")
}

三个 HandlerFunc 的注册顺序依次是 m1、m2、indexHandler

每个都调用了 c.Next()

所以他们的执行流应该是:

c.Abort()

c.Abort()c.Next() 的兄弟。

c.Next() 调用下一个 HandlerFunc; c.Abort() 阻止调用下一个 HandlerFunc。一旦阻止了,则后面不管有多少个 HandlerFunc,它们都没有机会执行了。

Example
// 中间件1
func m1(c *gin.Context) {
    fmt.Println("m1 in...")
    c.Next()
    fmt.Println("m1 out...")
}

// 中间件2
func m2(c *gin.Context) {
    fmt.Println("m2 in...")
    c.Abort()    // 阻止调用后面的 HandlerFunc
    fmt.Println("m2 out...")
}

// 中间件3
func m3(c *gin.Context) {
    fmt.Println("m3 in...")
    c.Next()
    fmt.Println("m3 out...")
}

// 首页处理函数
func indexHandler(c *gin.Context) {
    fmt.Println("Index handler in...")

    c.JSON(http.StatusOK, gin.H{
        "msg": "Here is index.",
    })
    c.Next()
    fmt.Println("Index handler out...")
}


func main() {
    r := gin.Default()

    r.GET("/", m1, m2, m3, indexHandler)    // 依次局部注册四个中间件

    r.Run(":9090")
}

m2 的时候调用了 c.Abort(),所以后面的 m3、indexHandler 都没有机会执行了。

中间件之间数据传递

中间件之间要传递数据,可以通过 c.Set(key, value) 的方式发送,在获取的地方用 c.Get(key) 的方式获取。

获取的方法有非常多,如下图:

在这里的获取要注意执行流的问题,如果第2个中间件执行 c.Set(),但是在第1个中间件就执行了 c.Get(),那会什么都拿不到。

Example
// 中间件1
func m1(c *gin.Context) {
    fmt.Println("m1 in...")
    c.Set("k", 123)    // 设置数据
    c.Next()
    fmt.Println("m1 out...")
}

// 首页处理函数
func indexHandler(c *gin.Context) {
    fmt.Println("Index handler in...")

    fmt.Println(c.MustGet("k"))    // 获取数据

    c.JSON(http.StatusOK, gin.H{
        "msg": "Here is index.",
    })

    fmt.Println("Index handler out...")
}


func main() {
    r := gin.Default()

    r.GET("/", m1, indexHandler)

    r.Run(":9090")
}

默认中间件

gin.Default() 默认使用了 LoggerRecovery 中间件,其中:

Logger中间件 将日志写入 gin.DefaultWriter,即使配置了 GIN_MODE=release

Recovery中间件 会 recover 任何 panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用 gin.New() 新建一个没有任何默认中间件的路由。

并发注意事项

gin中间件中使用goroutine

当在中间件或 handler 中启动新的 goroutine 时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())

func m1(c *gin.Context) {
    start := time.Now()
    end := time.Since(start)
    c.Set("timer", end)

    // go func(c)         // 错误!!!
    go func(c *gin.Context) { // 只能使用上下文的拷贝,不能使用原本的上下文
        fmt.Println("Other goroutine: ", c.MustGet("timer"))
    }(c.Copy())

}

func indexHandler(c *gin.Context) {
    timer := c.MustGet("timer")
    fmt.Println("indexHandler: ", timer)
}

func main() {
    r := gin.Default()

    r.GET("/", indexHandler)

    r.Run(":9090")
}