Go http.Handler wrapper and middleware 详解

http.Handler wrapper 中文

原文链接: The http.Handler wrapper technique in #golang UPDATED

http.Handler Wrapper 是只有一个输入和输出,且两者都为 http.Handler类型的函数。

Wrapper’s signature func(http.Handler) http.Handler

它的概念是: 函数接受 http.Handler 作为参数并且返回一个新的http.Handler,可以在 http.Handler 被调用前后做想做的事情,甚至可以决定最终是否调用传入的http.Handler

func log(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
		log.Println("Before")
		h.ServeHTTP(w, r) // call original
		log.Println("After")
	})
}

在这里, log 函数返回一个新的 http.Handler.(http.HandlerFunc 也是一个合法的 http.Handler). 它将先打印 “Before”. 然后在打印”after”之前调用传入的 handler.

然后这样使用 log

http.Handle("/path", handleThing)

变成

http.Handle("/path", log(handleThing))

什么时候使用 wrappers?

这个方法可以被用于很多地方,例如

是否调用原有 handler

Wrappers 需要决定是否调用传入的 handler。如果有需要,wrapper 甚至可以自己拦截请求。假设在 API 中keyURL参数是必须的:

func checkAPIKey(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if len(r.URL.Query().Get("key") == 0) {
      http.Error(w, "missing key", http.StatusUnauthorized)
      return // don't call original handler
    }
    h.ServeHTTP(w, r)
  })
}

checkAPIKey wrapper 会确认 url 里是否包含 key。如果没有,直接返回 Unauthorized错误。这个 wrapper 可以被扩展,例如:在数据仓库中校验 key。或者确认请求发送频率在可接受的范围之内.

Deferring (延迟)

通过 Go 的 defer 方法,我们可以确保无论传入的 handler 内发生了什么,都会被调用的代码:

func log(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Before")
    defer log.Println("After")
    h.ServeHTTP(w, r)
  })
}

这样,就算 h.ServeHTTP(w, r) 出现了 panic,我们依然可以看到 “After” 被打印出来.

向 wrapper 传递参数

我们可以通过向 wrapper 传递参数来实现更多的功能。例如之前的 checkAPIKey wrapper 可以被改为支持检测任意参数。而不仅仅是只能检测 key 这一个。

func MustParams(h http.Handler, params ...string) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){

    q := r.URL.Query()
    for _, param := range params {
      if len(q.Get(param)) == 0 {
        http.Error(w, "missing "+param, http.StatusBadRequest)
        return // exit early
      }
    }
    h.ServeHTTP(w, r) // all params present, proceed
  })
}

我们可以通过向 MustParams 传入不同的参数来检测不同的 parameter。params参数在返回的闭包函数中是可见的。所以不需要创建 struct 来存储状态。(原文: so there’s no need to add a struct to store state here. 个人理解是不需要额外显式传递 params 到内部函数).

可以这样调用 MustParams

http.Handler("/user", MustParams(handleUser, "key", "auth"))
http.Handler("/group", MustParams(handleGroup, "key"))
http.Handler("/items", MustParams(handleSearch, "key", "q"))

在 wrappers 中的 wrappers

考虑到这种模式的自相似性, 且实际上我们并没有更改 http.Handler的签名。我们可以很容易地把 wrappers 串联起来调用,如同把它们有趣地嵌套在一起.

假设我们有一个名为 MustAuth 的 wrapper。它的作用是校验请求中的 token。它需要 auth 参数必须存在。那么我们可以在 MustAuth中使用 MustParams:

func MustAuth(h http.Handler) http.Handler {
  checkauth := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
    err := validateAuth(r.URL.Query().Get("auth"))
    if err != nil {
      http.Error(w, "bad auth param", http.StatusUnauthorized)
    }
    h.ServeHTTP(w, r)
  })
  return MustParams(checkauth, "auth")
}

拦截 ResponseWriter

http.ResponseWriter是一个接口, 这就意味着我们可以拦截一个请求并把它转向另一个对象,只要我们提供了接口所需的方法。

你可以决定是否要捕获 response body,或者只是记录它

func log(h http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w := NewResponseLogger(w)
    h.ServeHTTP(w, r)
  })
}

NewResponseLogger将会提供它自己的 Write 函数并记录数据。

func (r *ResponseLogger) Write(b []byte) (int, error) {
  log.Print(string(b)) // log it out
  return r.w.Write(b) // pass it to the original ResponseWriter
}

这个函数记录了 response body 并且把读取出来的值又写入了初始的 http.ResponseWritter. 如此 API 的调用者可以依然得到 response body。(Go 中 response body 一次性读取的,读取之后就清空了)

Handlers, all the way down

除了像我们刚刚所做的包装单个handler之外,既然所有都是 http.Handler.我们可以把整个 server 包在一起,像这样:

http.ListenAndServe(addr, log(server))

关于更多 middleware 的信息,请查看 https://medium.com/@matryer/writing-middleware-in-golang-and-how-go-makes-it-so-much-fun-4375c1246e81

Back