7

Go语言http中间件

 3 years ago
source link: https://www.zenlife.tk/go-http-middleware.md
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2015-03-07

http中间件

Go语言的http的Handler很好写,下面是一个helloworld例子:

func helloworld(w http.ResponseWriter, r *http.Request) { io.Write(w, "hello world!") }

http中间件是指在原来http.Handler的基础上,包装一层,得到一个新的Handler。

func GETMethodFilter(h http.Handler) http.Handler { return http.HandlerFunc(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.NotFound(w, r) return } h.ServeHTTP(w, r) } }

任何一个函数,如果它可以接受一个http.Handler,并返回一个新的Handler,这个函数就可以算是一个中间件。GETMethodFilter就是一个http中间件,我们可以将它包装到helloworld上面,得到新的Handler是一个只处理GET方法的helloworld。

GETMethodFilter(helloworld)

http中间件最大的好处是无侵入性,只要愿意,可以在外面嵌套许许多多层中间件,达到不同的目标。比较可以加上登陆,加上参数合法性校验,加上访问权限过滤,等等等。

上面写过一个GETMethodFilter的例子,那么如果我们要过滤掉的是POST方法呢?重复代码是不好的事情。我们可以写一个MethodFilter,接受参数GET或者POST来决定是返回GETMethodFilter或者POSTMethodFilter。

func MethodFilter(method string, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != method { http.NotFound(w, r) return } h.ServeHTTP(w, r) }) }

这种写法有缺陷,多了一个参数,它就不是我们之前的中间件形式了,这种不一致这会导致后面写串联函数不方便。我们将只能这样写:

ParamFilter("some argument", MethodFilter("GET", helloworld))

而无法写成:

New(helloworld).Then(ParamFilter).Then(MethodFilter)

因为不同的方法需要不同的参数。

Go语言中推崇组合。为了真正实现串联,必须要再往上抽象一层。任何东西只要实现了MiddleWare接口,它就是一个MiddleWare。

type MiddleWare interface { Chain(http.Handler) http.Handler }

MiddleWare和MiddlewareFunc的关系,很类似标准库中Handler跟HandlerFunc的关系。

type MiddlewareFunc func(http.Handler) http.Handler

将MethodFilter修改成为一个真正的MiddleWare:

func MethodFilter(method string) MiddleWare { return MiddlewareFunc(func(base http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != method { http.NotFound(w, r) } base.ServeHTTP(w, r) }) }) }

上面函数式的写法,如果换成面向对象的写法,我们可以写成下面形式:

type MethodFilter struct { method string } func (m *MethodFilter) Chain(http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if m.method != method { // xxx } base.ServeHTTP(w, r) }) }

对象是穷人的闭包?闭包是穷人的对象?who cares。不过在Go语言中,我直觉上认为后一种写法性能上应该好一点点,说来话长所以不解释。

用MiddleWare做串联就很简单了。比如说:

type Chain struct { middlewares []MiddleWare raw http.Handler } func New(handler http.Handler, middlewares ...MiddleWare) *Chain { return &Chain{ raw: handler, middlewares: middlewares, } } func (mc Chain) Then(m MiddleWare) Chain { mc.middlewares = append(mc.middlewares, m) return mc } func (mc MiddleWareChain) ServeHTTP(w http.ResponseWriter, r http.Request) { final := mc.raw for _, v := range mc.middlewares { final = v.Chain(final) } final.ServeHTTP(w, r) }

然后这样子用

New(helloworld).Then(ParamFilter).Then(MethodFilter)

如果使用http中间件,必然会遇到的一个问题是上下文中传递数据的问题。上下文是指什么呢?每个中间件会从前面的中间件中获取数据,并且可能会生成新的数据传后面的中间件,这些数据传递形成的就是上下文。比如我有一个ParamFilter,这个中间件的作用是验证输出参数是否合法的。那么验证完之后,应该把数据放到上下文中,传给后面中间件去使用。再比如说,如果我写了一个Login的中间件,那么这个中间件可以把session一类的信息就可以放到上下文,后面的中间件就可以从上下文中获取到session。

gorilla的做法是把上下文信息放到了一个全局map中,使用http.Request作为key就可以将上下文获取出来。这个方案优点是对标准库无侵入性。但是也有些缺点,一个是请求结束后还需要清除掉map[r],另一个是全局map的加锁会影响性能,这个缺点在很多场景是致命的。

Go语言官方推荐的做法是,在每个Handler都加多一个参数context。比如写一个

type ContextHandler interface { ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) }

官方的补充库中专门有一个context.Context接口。这种方式对标准库的Handler侵入性比较强,如果有较多遗留代码,也不算太好一个方案。

使用http中间件,处理上下文是必不可少的。上面都是比较有代表性的方案,但是我都不太满意。直到突然有一天灵光一闪,发现其实可以利用http.ResponseWriter是interface这点,把上下文隐藏在里面!

type ContextResponseWriter interface { http.ResponseWriter Value(interface{}) interface{} }

这样我们可以写标准的http.Handler接口,并且在需要的时候又可以取出上下文:

func HelloWorld(w http.ResponseWriter, r *http.Request) { if cw, ok := w.(ContextResponseWriter); ok { value := cw.Value("key") ... } }

package main

import ( "github.com/tiancaiamao/middleware" "io" "net/http" )

type MyContextResponseWriter struct { http.ResponseWriter key, value interface{} }

func (w *MyContextResponseWriter) Value(key interface{}) interface{} { if w.key == key { return w.value } return nil }

func HelloWorld(w http.ResponseWriter, r *http.Request) { if ctx, ok := w.(middleware.ContextResponseWriter); ok { valueFromContext := ctx.Value("xxx") io.WriteString(w, "hello, "+valueFromContext.(string)) return }

io.WriteString(w, "hello world") }

type MiddleWareDemo struct{}

func (demo MiddleWareDemo) Chain(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cw := &MyContextResponseWriter{ ResponseWriter: w, key: "xxx", value: "demo", }

h.ServeHTTP(cw, r) }) }

func main() { handler := middleware.New(http.HandlerFunc(HelloWorld), MiddleWareDemo{}) http.ListenAndServe(":8080", handler) }

代码放到了github,需要的自取。


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK