简介

上一篇文章中,我们介绍了 gorilla web 开发工具包中的路由管理库gorilla/mux,在文章最后我们介绍了如何使用中间件处理通用的逻辑。在日常 Go Web 开发中,开发者遇到了很多相同的中间件需求,gorilla/handlers(后文简称为handlers)收集了一些比较常用的中间件。一起来看看吧~

关于中间件,前面几篇文章已经介绍的很多了。这里就不赘述了。handlers库提供的中间件可用于标准库net/http和所有支持http.Handler接口的框架。由于gorilla/mux也支持http.Handler接口,所以也可以与handlers库结合使用。这就是兼容标准的好处

项目初始化&安装

本文代码使用 Go Modules。

创建目录并初始化:

  1. $ mkdir -p gorilla/handlers && cd gorilla/handlers
  2. $ go mod init github.com/go-quiz/go-daily-lib/gorilla/handlers

安装gorilla/handlers库:

  1. $ go get -u github.com/gorilla/handlers

下面依次介绍各个中间件和相应的源码。

日志

handlers提供了两个日志中间件:

  • LoggingHandler:以 Apache 的Common Log Format日志格式记录 HTTP 请求日志;
  • CombinedLoggingHandler:以 Apache的Combined Log Format日志格式记录 HTTP 请求日志,Apache 和 Nginx 默认都使用这种日志格式。

两种日志格式差别很小,Common Log Format格式如下:

  1. %h %l %u %t "%r" %>s %b

各个指示符含义如下:

  • %h:客户端的 IP 地址或主机名;
  • %lRFC 1413定义的客户端标识,由客户端机器上的identd程序生成。如果不存在,则该字段为-
  • %u:已验证的用户名。如果不存在,该字段为-
  • %t:时间,格式为day/month/year:hour:minute:second zone,其中:

    • day: 2位数字;
    • month:月份缩写,3个字母,如Jan
    • year:4位数字;
    • hour:2位数字;
    • minute:2位数字;
    • second:2位数字;
    • zone+-后跟4位数字;
    • 例如:21/Jul/2021:06:27:33 +0800
  • %r:包含 HTTP 请求行信息,例GET /index.html HTTP/1.1
  • %>s:服务器发送给客户端的状态码,例如200
  • %b:响应长度(字节数)。

Combined Log Format格式如下:

  1. %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-Agent}i"

可见相比Common Log Format只是多了:

  • %{Referer}i:HTTP 首部中的Referer信息;
  • %{User-Agent}i:HTTP 首部中的User-Agent信息。

对中间件,我们可以让它作用于全局,即全部处理器,也可以让它只对某些处理器生效。如果要对所有处理器生效,可以调用Use()方法。如果只需要作用于特定的处理器,在注册时用中间件将处理器包装一层:

  1. func index(w http.ResponseWriter, r *http.Request) {
  2. fmt.Fprintln(w, "Hello World")
  3. }
  4. type greeting string
  5. func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  6. fmt.Fprintf(w, "Welcome, %s", g)
  7. }
  8. func main() {
  9. r := mux.NewRouter()
  10. r.Handle("/", handlers.LoggingHandler(os.Stdout, http.HandlerFunc(index)))
  11. r.Handle("/greeting", handlers.CombinedLoggingHandler(os.Stdout, greeting("dj")))
  12. http.Handle("/", r)
  13. log.Fatal(http.ListenAndServe(":8080", nil))
  14. }

上面代码中LoggingHandler只作用于处理函数indexCombinedLoggingHandler只作用于处理器greeting("dj")

运行代码,通过浏览器访问localhost:8080localhost:8080/greeting

  1. ::1 - - [21/Jul/2021:06:39:45 +0800] "GET / HTTP/1.1" 200 12
  2. ::1 - - [21/Jul/2021:06:39:54 +0800] "GET /greeting HTTP/1.1" 200 11 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.164 Safari/537.36"

对照前面分析的指示符,很容易看出各个部分。

由于*mux.RouterUse()方法接受类型为MiddlewareFunc的中间件:

  1. type MiddlewareFunc func(http.Handler) http.Handler

handlers.LoggingHandler/CombinedLoggingHandler并不满足,所以还需要包装一层才能传给Use()方法:

  1. func Logging(handler http.Handler) http.Handler {
  2. return handlers.CombinedLoggingHandler(os.Stdout, handler)
  3. }
  4. func main() {
  5. r := mux.NewRouter()
  6. r.Use(Logging)
  7. r.HandleFunc("/", index)
  8. r.Handle("/greeting/", greeting("dj"))
  9. http.Handle("/", r)
  10. log.Fatal(http.ListenAndServe(":8080", nil))
  11. }

另外handlers还提供了CustomLoggingHandler,我们可以利用它定义自己的日志中间件:

  1. func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler

最关键的LogFormatter类型定义:

  1. type LogFormatterParams struct {
  2. Request *http.Request
  3. URL url.URL
  4. TimeStamp time.Time
  5. StatusCode int
  6. Size int
  7. }
  8. type LogFormatter func(writer io.Writer, params LogFormatterParams)

我们实现一个简单的LogFormatter,记录时间 + 请求行 + 响应码:

  1. func myLogFormatter(writer io.Writer, params handlers.LogFormatterParams) {
  2. var buf bytes.Buffer
  3. buf.WriteString(time.Now().Format("2006-01-02 15:04:05 -0700"))
  4. buf.WriteString(fmt.Sprintf(` "%s %s %s" `, params.Request.Method, params.URL.Path, params.Request.Proto))
  5. buf.WriteString(strconv.Itoa(params.StatusCode))
  6. buf.WriteByte('\n')
  7. writer.Write(buf.Bytes())
  8. }
  9. func Logging(handler http.Handler) http.Handler {
  10. return handlers.CustomLoggingHandler(os.Stdout, handler, myLogFormatter)
  11. }

使用:

  1. func main() {
  2. r := mux.NewRouter()
  3. r.Use(Logging)
  4. r.HandleFunc("/", index)
  5. r.Handle("/greeting/", greeting("dj"))
  6. http.Handle("/", r)
  7. log.Fatal(http.ListenAndServe(":8080", nil))
  8. }

现在记录的日志是下面这种格式:

  1. 2021-07-21 07:03:18 +0800 "GET /greeting/ HTTP/1.1" 200

翻看源码,我们可以发现LoggingHandler/CombinedLoggingHandler/CustomLoggingHandler都是基于底层的loggingHandler实现的,不同的是LoggingHandler使用了预定义的writeLog作为LogFormatterCombinedLoggingHandler使用了预定义的writeCombinedLog作为LogFormatter,而CustomLoggingHandler使用我们自己定义的LogFormatter

  1. func CombinedLoggingHandler(out io.Writer, h http.Handler) http.Handler {
  2. return loggingHandler{out, h, writeCombinedLog}
  3. }
  4. func LoggingHandler(out io.Writer, h http.Handler) http.Handler {
  5. return loggingHandler{out, h, writeLog}
  6. }
  7. func CustomLoggingHandler(out io.Writer, h http.Handler, f LogFormatter) http.Handler {
  8. return loggingHandler{out, h, f}
  9. }

预定义的writeLog/writeCombinedLog实现如下:

  1. func writeLog(writer io.Writer, params LogFormatterParams) {
  2. buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
  3. buf = append(buf, '\n')
  4. writer.Write(buf)
  5. }
  6. func writeCombinedLog(writer io.Writer, params LogFormatterParams) {
  7. buf := buildCommonLogLine(params.Request, params.URL, params.TimeStamp, params.StatusCode, params.Size)
  8. buf = append(buf, ` "`...)
  9. buf = appendQuoted(buf, params.Request.Referer())
  10. buf = append(buf, `" "`...)
  11. buf = appendQuoted(buf, params.Request.UserAgent())
  12. buf = append(buf, '"', '\n')
  13. writer.Write(buf)
  14. }

它们都是基于buildCommonLogLine构造基本信息,writeCombinedLog还分别调用http.Request.Referer()http.Request.UserAgent获取了RefererUser-Agent信息。

loggingHandler定义如下:

  1. type loggingHandler struct {
  2. writer io.Writer
  3. handler http.Handler
  4. formatter LogFormatter
  5. }

loggingHandler实现有一个比较巧妙的地方:为了记录响应码和响应大小,定义了一个类型responseLogger包装原来的http.ResponseWriter,在写入时记录信息:

  1. type responseLogger struct {
  2. w http.ResponseWriter
  3. status int
  4. size int
  5. }
  6. func (l *responseLogger) Write(b []byte) (int, error) {
  7. size, err := l.w.Write(b)
  8. l.size += size
  9. return size, err
  10. }
  11. func (l *responseLogger) WriteHeader(s int) {
  12. l.w.WriteHeader(s)
  13. l.status = s
  14. }
  15. func (l *responseLogger) Status() int {
  16. return l.status
  17. }
  18. func (l *responseLogger) Size() int {
  19. return l.size
  20. }

loggingHandler的关键方法ServeHTTP()

  1. func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  2. t := time.Now()
  3. logger, w := makeLogger(w)
  4. url := *req.URL
  5. h.handler.ServeHTTP(w, req)
  6. if req.MultipartForm != nil {
  7. req.MultipartForm.RemoveAll()
  8. }
  9. params := LogFormatterParams{
  10. Request: req,
  11. URL: url,
  12. TimeStamp: t,
  13. StatusCode: logger.Status(),
  14. Size: logger.Size(),
  15. }
  16. h.formatter(h.writer, params)
  17. }

构造LogFormatterParams对象,调用对应的LogFormatter函数。

压缩

如果客户端请求中有Accept-Encoding首部,服务器可以使用该首部指示的算法将响应压缩,以节省网络流量。handlers.CompressHandler中间件启用压缩功能。还有一个CompressHandlerLevel可以指定压缩级别。实际上CompressHandler就是使用gzip.DefaultCompression调用的CompressHandlerLevel

  1. func CompressHandler(h http.Handler) http.Handler {
  2. return CompressHandlerLevel(h, gzip.DefaultCompression)
  3. }

看代码:

  1. func index(w http.ResponseWriter, r *http.Request) {
  2. fmt.Fprintln(w, "Hello World")
  3. }
  4. type greeting string
  5. func (g greeting) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  6. fmt.Fprintf(w, "Welcome, %s", g)
  7. }
  8. func main() {
  9. r := mux.NewRouter()
  10. r.Use(handlers.CompressHandler)
  11. r.HandleFunc("/", index)
  12. r.Handle("/greeting/", greeting("dj"))
  13. http.Handle("/", r)
  14. log.Fatal(http.ListenAndServe(":8080", nil))
  15. }

运行,请求localhost:8080,通过 Chrome 开发者工具的 Network 页签可以看到响应采用了 gzip 压缩:

每日一库之74:gorilla-handlers - 图1

忽略一些细节处理,CompressHandlerLevel函数代码如下:

  1. func CompressHandlerLevel(h http.Handler, level int) http.Handler {
  2. const (
  3. gzipEncoding = "gzip"
  4. flateEncoding = "deflate"
  5. )
  6. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  7. var encoding string
  8. for _, curEnc := range strings.Split(r.Header.Get(acceptEncoding), ",") {
  9. curEnc = strings.TrimSpace(curEnc)
  10. if curEnc == gzipEncoding || curEnc == flateEncoding {
  11. encoding = curEnc
  12. break
  13. }
  14. }
  15. if encoding == "" {
  16. h.ServeHTTP(w, r)
  17. return
  18. }
  19. if r.Header.Get("Upgrade") != "" {
  20. h.ServeHTTP(w, r)
  21. return
  22. }
  23. var encWriter io.WriteCloser
  24. if encoding == gzipEncoding {
  25. encWriter, _ = gzip.NewWriterLevel(w, level)
  26. } else if encoding == flateEncoding {
  27. encWriter, _ = flate.NewWriter(w, level)
  28. }
  29. defer encWriter.Close()
  30. w.Header().Set("Content-Encoding", encoding)
  31. r.Header.Del(acceptEncoding)
  32. cw := &compressResponseWriter{
  33. w: w,
  34. compressor: encWriter,
  35. }
  36. w = httpsnoop.Wrap(w, httpsnoop.Hooks{
  37. Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {
  38. return cw.Write
  39. },
  40. WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
  41. return cw.WriteHeader
  42. },
  43. Flush: func(httpsnoop.FlushFunc) httpsnoop.FlushFunc {
  44. return cw.Flush
  45. },
  46. ReadFrom: func(rff httpsnoop.ReadFromFunc) httpsnoop.ReadFromFunc {
  47. return cw.ReadFrom
  48. },
  49. })
  50. h.ServeHTTP(w, r)
  51. })
  52. }

从请求Accept-Encoding首部中获取客户端指示的压缩算法。如果客户端未指定,或请求首部中有Upgrade,则不压缩。反之,则压缩。根据识别的压缩算法,创建对应gzipflateio.Writer实现对象。

与前面的日志中间件一样,为了压缩写入的内容,新增类型compressResponseWriter封装http.ResponseWriter,重写Write()方法,将写入的字节流传入前面创建的io.Writer实现压缩:

  1. type compressResponseWriter struct {
  2. compressor io.Writer
  3. w http.ResponseWriter
  4. }
  5. func (cw *compressResponseWriter) Write(b []byte) (int, error) {
  6. h := cw.w.Header()
  7. if h.Get("Content-Type") == "" {
  8. h.Set("Content-Type", http.DetectContentType(b))
  9. }
  10. h.Del("Content-Length")
  11. return cw.compressor.Write(b)
  12. }

内容类型

我们可以通过handler.ContentTypeHandler指定请求的Content-Type必须在我们给出的类型中,只对POST/PUT/PATCH方法生效。例如我们限制登录请求必须通过application/x-www-form-urlencoded的形式发送:

  1. func main() {
  2. r := mux.NewRouter()
  3. r.HandleFunc("/", index)
  4. r.Methods("GET").Path("/login").HandlerFunc(login)
  5. r.Methods("POST").Path("/login").
  6. Handler(handlers.ContentTypeHandler(http.HandlerFunc(dologin), "application/x-www-form-urlencoded"))
  7. http.Handle("/", r)
  8. log.Fatal(http.ListenAndServe(":8080", nil))
  9. }

这样,只要请求/loginContent-Type不是application/x-www-form-urlencoded就会返回 415 错误。我们可以故意写错,再请求看看表现:

  1. Unsupported content type "application/x-www-form-urlencoded"; expected one of ["application/x-www-from-urlencoded"]

ContentTypeHandler的实现非常简单:

  1. func ContentTypeHandler(h http.Handler, contentTypes ...string) http.Handler {
  2. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3. if !(r.Method == "PUT" || r.Method == "POST" || r.Method == "PATCH") {
  4. h.ServeHTTP(w, r)
  5. return
  6. }
  7. for _, ct := range contentTypes {
  8. if isContentType(r.Header, ct) {
  9. h.ServeHTTP(w, r)
  10. return
  11. }
  12. }
  13. http.Error(w, fmt.Sprintf("Unsupported content type %q; expected one of %q", r.Header.Get("Content-Type"), contentTypes), http.StatusUnsupportedMediaType)
  14. })
  15. }

就是读取Content-Type首部,判断是否在我们指定的类型中。

方法分发器

在上面的例子中,我们注册路径/loginGETPOST方法处理采用r.Methods("GET").Path("/login").HandlerFunc(login)这种冗长的写法。handlers.MethodHandler可以简化这种写法:

  1. func main() {
  2. r := mux.NewRouter()
  3. r.HandleFunc("/", index)
  4. r.Handle("/login", handlers.MethodHandler{
  5. "GET": http.HandlerFunc(login),
  6. "POST": http.HandlerFunc(dologin),
  7. })
  8. http.Handle("/", r)
  9. log.Fatal(http.ListenAndServe(":8080", nil))
  10. }

MethodHandler底层是一个map[string]http.Handler类型,它的ServeHTTP()方法根据请求的 Method 调用不同的处理:

  1. type MethodHandler map[string]http.Handler
  2. func (h MethodHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  3. if handler, ok := h[req.Method]; ok {
  4. handler.ServeHTTP(w, req)
  5. } else {
  6. allow := []string{}
  7. for k := range h {
  8. allow = append(allow, k)
  9. }
  10. sort.Strings(allow)
  11. w.Header().Set("Allow", strings.Join(allow, ", "))
  12. if req.Method == "OPTIONS" {
  13. w.WriteHeader(http.StatusOK)
  14. } else {
  15. http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  16. }
  17. }
  18. }

方法如果未注册,则返回405 Method Not Allowed。有一个方法除外,OPTIONS。该方法通过Allow首部返回支持哪些方法。

重定向

handlers.CanonicalHost可以将请求重定向到指定的域名,同时指定重定向响应码。在同一个服务器对应多个域名时比较有用:

  1. func index(w http.ResponseWriter, r *http.Request) {
  2. fmt.Fprintln(w, "hello world")
  3. }
  4. func main() {
  5. r := mux.NewRouter()
  6. r.Use(handlers.CanonicalHost("http://www.gorillatoolkit.org", 302))
  7. r.HandleFunc("/", index)
  8. http.Handle("/", r)
  9. log.Fatal(http.ListenAndServe(":8080", nil))
  10. }

上面将所有请求以 302 重定向到http://www.gorillatoolkit.org

CanonicalHost的实现也很简单:

  1. func CanonicalHost(domain string, code int) func(h http.Handler) http.Handler {
  2. fn := func(h http.Handler) http.Handler {
  3. return canonical{h, domain, code}
  4. }
  5. return fn
  6. }

关键类型canonical

  1. type canonical struct {
  2. h http.Handler
  3. domain string
  4. code int
  5. }

核心方法:

  1. func (c canonical) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  2. dest, err := url.Parse(c.domain)
  3. if err != nil {
  4. c.h.ServeHTTP(w, r)
  5. return
  6. }
  7. if dest.Scheme == "" || dest.Host == "" {
  8. c.h.ServeHTTP(w, r)
  9. return
  10. }
  11. if !strings.EqualFold(cleanHost(r.Host), dest.Host) {
  12. dest := dest.Scheme + "://" + dest.Host + r.URL.Path
  13. if r.URL.RawQuery != "" {
  14. dest += "?" + r.URL.RawQuery
  15. }
  16. http.Redirect(w, r, dest, c.code)
  17. return
  18. }
  19. c.h.ServeHTTP(w, r)
  20. }

由源码可知,域名不合法或未指定协议(Scheme)或域名(Host)的请求下不转发。

Recovery

之前我们自己实现了PanicRecover中间件,避免请求处理时 panic。handlers提供了一个RecoveryHandler可以直接使用:

  1. func PANIC(w http.ResponseWriter, r *http.Request) {
  2. panic(errors.New("unexpected error"))
  3. }
  4. func main() {
  5. r := mux.NewRouter()
  6. r.Use(handlers.RecoveryHandler(handlers.PrintRecoveryStack(true)))
  7. r.HandleFunc("/", PANIC)
  8. http.Handle("/", r)
  9. log.Fatal(http.ListenAndServe(":8080", nil))
  10. }

选项PrintRecoveryStack表示 panic 时输出堆栈信息。

RecoveryHandler的实现与之前我们自己编写的基本一样:

  1. type recoveryHandler struct {
  2. handler http.Handler
  3. logger RecoveryHandlerLogger
  4. printStack bool
  5. }
  6. func (h recoveryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  7. defer func() {
  8. if err := recover(); err != nil {
  9. w.WriteHeader(http.StatusInternalServerError)
  10. h.log(err)
  11. }
  12. }()
  13. h.handler.ServeHTTP(w, req)
  14. }

总结

GitHub 上有很多开源的 Go Web 中间件实现,可以直接拿来使用,避免重复造轮子。handlers很轻量,容易与标准库net/http和 gorilla 路由库mux结合使用。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. gorilla/handlers GitHub:github.com/gorilla/handlers
  2. Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib