十分钟学会用Go编写Web中间件

中间件(通常)是一小段代码,它们接受一个请求,对其进行处理,每个中间件只处理一件事情,完成后将其传递给另一个中间件或最终处理程序,这样就做到了程序的解耦。如果没有中间件那么我们必须在最终的处理程序中来完成这些处理操作,这无疑会造成处理程序的臃肿和代码复用率不高的问题。中间件的一些常见用例是请求日志记录,Header操纵、HTTP请求认证和ResponseWriter劫持等等。


画外音:上面这段描述中间件的文字,跟我两年前在Laravel源码解析之中间件写的几乎一样(其实这图也是从那里拿过来的)。再次说明做开发时间长了以后掌握一些编程的思想有时候比掌握一门编程语言更重要,这不咱们就又用Go来写中间件了。

创建中间件

接下来我们用Go创建中间件,中间件只将http.HandlerFunc作为其参数,在中间件里将其包装并返回新的http.HandlerFunc供服务器服务复用器调用。这里我们创建一个新的类型Middleware,这会让最后一起链式调用多个中间件变的更简单。

type Middleware func(http.HandlerFunc) http.HandlerFunc

下面的中间件通用代码模板让我们平时编写中间件变得更容易。

中间件代码模板

中间件是使用装饰器模式实现的,下面的中间件通用代码模板让我们平时编写中间件变得更容易,我们在自己写中间件的时候只需要往样板里填充需要的代码逻辑即可。

func createNewMiddleware() Middleware {

    // 创建一个新的中间件

    middleware := func(next http.HandlerFunc) http.HandlerFunc {

        // 创建一个新的handler包裹next

        handler := func(w http.ResponseWriter, r *http.Request) {

            // 中间件的处理逻辑

......

            // 调用下一个中间件或者最终的handler处理程序

            next(w, r)

        }

        // 返回新建的包装handler

        return handler

    }

    // 返回新建的中间件

    return middleware

}

使用中间件

我们创建两个中间件,一个用于记录程序执行的时长,另外一个用于验证请求用的是否是指定的HTTP Method,创建完后再用定义的Chain函数把http.HandlerFunc和应用在其上的中间件链起来,中间件会按添加顺序依次执行,最后执行到处理函数。完整的代码如下:

package main

import (

    "fmt"

    "log"

    "net/http"

    "time"

)

type Middleware func(http.HandlerFunc) http.HandlerFunc

// 记录每个URL请求的执行时长

func Logging() Middleware {

    // 创建中间件

    return func(f http.HandlerFunc) http.HandlerFunc {

        // 创建一个新的handler包装http.HandlerFunc

        return func(w http.ResponseWriter, r *http.Request) {

            // 中间件的处理逻辑

            start := time.Now()

            defer func() { log.Println(r.URL.Path, time.Since(start)) }()

            // 调用下一个中间件或者最终的handler处理程序

            f(w, r)

        }

    }

}

// 验证请求用的是否是指定的HTTP Method,不是则返回 400 Bad Request

func Method(m string) Middleware {

    return func(f http.HandlerFunc) http.HandlerFunc {

        return func(w http.ResponseWriter, r *http.Request) {

            if r.Method != m {

                http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)

                return

            }

            f(w, r)

        }

    }

}

// 把应用到http.HandlerFunc处理器的中间件

// 按照先后顺序和处理器本身链起来供http.HandleFunc调用

func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {

    for _, m := range middlewares {

        f = m(f)

    }

    return f

}

// 最终的处理请求的http.HandlerFunc

func Hello(w http.ResponseWriter, r *http.Request) {

    fmt.Fprintln(w, "hello world")

}

func main() {

    http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))

    http.ListenAndServe(":8080", nil)

}

运行程序后会打开浏览器访问http://localhost:8080会有如下输出:

2020/02/07 21:07:52 / 359.503µs

2020/02/07 21:09:17 / 34.727µs

到这里怎么用Go编写和使用中间件就讲完,也就十分钟吧。不过这里更多的是探究实现原理,那么在生产环境怎么自己使用编写的这些中间件呢,我们接着往下看。

使用gorilla/mux应用中间件

上面我们探讨了如何创建中间件,但是使用上每次用Chain函数链接多个中间件和处理程序还是有些不方便,而且在上一篇文章中我们已经开始使用gorilla/mux提供的Router作为路由器了。好在gorrila.mux支持向路由器添加中间件,如果发现匹配项,则按照添加中间件的顺序执行中间件,包括其子路由器也支持添加中间件。

gorrila.mux路由器使用Use方法为路由器添加中间件,Use方法的定义如下:

func (r *Router) Use(mwf ...MiddlewareFunc) {

for _, fn := range mwf {

r.middlewares = append(r.middlewares, fn)

}

}

它可以接受多个mux.MiddlewareFunc类型的参数,mux.MiddlewareFunc的类型声明为:

type MiddlewareFunc func(http.Handler) http.Handler

跟我们上面定义的Middleware类型很像也是一个函数类型,不过函数的参数和返回值都是http.Handler接口,在《深入学习用 Go 编写 HTTP 服务器》中我们详细讲过http.Handler它 是net/http中定义的接口用来表示处理 HTTP 请求的对象,其对象必须实现ServeHTTP方法。我们把上面说的中间件模板稍微更改下就能创建符合gorrila.mux要求的中间件:

func CreateMuxMiddleware() mux.MiddlewareFunc {

// 创建中间件

return func(f http.Handler) http.Handler {

// 创建一个新的handler包装http.HandlerFunc

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// 中间件的处理逻辑

......

// 调用下一个中间件或者最终的handler处理程序

f.ServeHTTP(w, r)

})

}

}

接下来,我们把上面自定义的两个中间件进行改造,然后应用到我们一直在使用的http_demo项目上,为了便于管理在项目中新建middleware目录,两个中间件分别放在log.go和http_method.go中

//middleware/log.go

func Logging() mux.MiddlewareFunc {

// 创建中间件

return func(f http.Handler) http.Handler {

// 创建一个新的handler包装http.HandlerFunc

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// 中间件的处理逻辑

start := time.Now()

defer func() { log.Println(r.URL.Path, time.Since(start)) }()

// 调用下一个中间件或者最终的handler处理程序

f.ServeHTTP(w, r)

})

}

}

// middleware/http_demo.go

func Method(m string) mux.MiddlewareFunc {

return func(f http.Handler) http.Handler {

return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

if r.Method != m {

http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)

return

}

f.ServeHTTP(w, r)

})

}

}

然后在我们的路由器中进行引用:

func RegisterRoutes(r *mux.Router) {

r.Use(middleware.Logging())// 全局应用

indexRouter := r.PathPrefix("/index").Subrouter()

indexRouter.Handle("/", &handler.HelloHandler{})

userRouter := r.PathPrefix("/user").Subrouter()

userRouter.HandleFunc("/names/{name}/countries/{country}", handler.ShowVisitorInfo)

userRouter.Use(middleware.Method("GET"))//给子路由器应用

}

再次编译启动运行程序后访问

http://localhost:8080/user/names/James/countries/NewZealand

从控制台里可以看到,记录了这个请求的处理时长:

2020/02/08 09:29:50 Starting HTTP server...

2020/02/08 09:55:20 /user/names/James/countries/NewZealan 51.157µs

到这里我们探究完了编写Web中间件的过程和原理,在实际开发中只需要根据自己的需求按照我们给的中间件代码模板编写中间件即可,在编写中间件的时候也要注意他们的职责范围,不要所有逻辑都往里放。


前期我也和很多小伙伴一样,到处收集了很多资料,后面发现很多重复的!上面面都是自己整理好的!现在BAT梦想成真,我就把资料贡献出来给有需要的人!

顺便求一波关注,哈哈~各位小伙伴关注我后私信【Java】就可以免费领取哒!

作者:kevinyan

原文链接:https://juejin.im/post/5e3e9884f265da57375c3138

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容