Go语言http中间件
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.
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
-
46
-
44
Negroni是一个非常棒的中间件,尤其是其中间件调用链优雅的设计,以及对GO HTTP 原生处理器的兼容。我以前写过两篇文章,对Negroni进行了专门的分析,没有看过的朋友可以在看下。 Go语言经典库使用分析(五)| Negroni 中间件(...
-
45
-
28
http 编程 Go 原生支持http: import "net/http" Go 的http服务性能和nginx比较接近: 就是说用Go写的Web程序上线,程序前面不需要再部署nginx的Web服务器,这里省掉的是Web服务器...
-
37
当用户的请求到来时,我们需要将用户的请求分散到多个服务去各自处理,然后又需要将这些子服务的结果汇总起来呈现给用户。那么服务之间该使用何种方式进行交互就是需要解决的核心问题。RPC 就是为解决服务之间信息交互而发明和存在的。
-
3
本篇文章来分析一下 Go 语言 HTTP 标准库是如何实现的。 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:https://www.luozhiyun.com/archives/561 本文使用的go的源码1....
-
5
几乎所有的编程语言都以Hello World作为入门程序的示例,其中有一部分以编写一个 Web 服务器作为实战案例的开始。每种编程语言都有很多用于编写 Web 服务器的库,或以标准库,或通过第三方库的方式提供。Go 语言也不例外。本文及后续的文章就去探...
-
7
使用go语言创建HTTP(s)代理100行代码 评分: 4.5 作者: Ryan Lu 类别: golang
-
4
-
17
http://http://http://@http://http://?http://#http:// | daniel.haxx.se
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK