Go語言的Http 中間件實現

jopen 8年前發布 | 17K 次閱讀 HTTP 中間件 Go語言 Google Go/Golang開發

英文原文鏈接: http://www.alexedwards.net/blog/making-and-using-middleware

當你正在構建一個Web應用程序有可能要運行許多(甚至全部)的HTTP請求一些共享功能,你可能想記錄每一個request,gzip壓縮的每個response,或者做一些繁重的處理或者緩存檢查。

實現這個共享功能的一種方法是將其設置為中間件,他可以作為一個獨立的程序,在正常的handlers處理之前。根本不需要重寫代碼:如果你想用一個中間件,就把它加上應用中;如果你改變主意了,去掉就好了。就這么簡單。

ServeMux => Middleware Handler => Application Handler
ServeMux => MiddlewareHandler => ApplicationHandler

這篇文章,我會給大家介紹怎么自己去實現一個自定義的middleware模式。以及通過使用第三方的中間件軟件包的一些具體的實例。

基本原則:

在Go語言中實現和使用middleware是非常簡單的。

  • 使我們的中間件能搞滿足 http.handlers 這個接口
  • 建立一個 handlers 鏈,使其能夠滿足中間件的 handler 和 正常應用的 handler,并且能夠注冊到 http.ServeMux

我來解釋如何實現:

首先你要知道go 的http handle,這里假設你是知道的

func messageHandler(message string) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(message)
  })
}
funcmessageHandler(messagestring) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte(message)
  })
}

這上面這個代碼片段里面我們的邏輯很簡單只是一個簡單的 w.Write() 然后我們使用 http.HandlerFunc 適配器來轉化這個閉包,并返回。

我們可以使用一個相同的方法來創建一個 handler 鏈。可以使用 handler 代替參數 string 傳進閉包,然后把控制 handler 給傳進來的 handler,并且調用 ServeHTTP() 方法。

這給了我們一個完整的模式構建中間件:

func exampleMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Our middleware logic goes here...
    next.ServeHTTP(w, r)
  })
}
funcexampleMiddleware(nexthttp.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Our middleware logic goes here...
    next.ServeHTTP(w, r)
  })
}

你注意到這個中間件有一個這樣的函數結構 func(http.Handler) http.Handler 。它接受一個 handler 作為參數,并且返回一個 handler。這里有兩個很有用的原因:

  • 因為這個函數返回一個句柄可以直接供中間件注冊
  • 我們可以建立任意長度的 handler 鏈來通過中間件的方法互相嵌套

比如:

http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))

控制流說明:

讓我們來看一個帶有多個中間件的例子,并且把日志輸出到控制臺:

package main

import (
  "log"
  "net/http"
)

func middlewareOne(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareOne")
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareOne again")
  })
}

func middlewareTwo(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareTwo")
    if r.URL.Path != "/" {
      return
    }
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareTwo again")
  })
}

func final(w http.ResponseWriter, r *http.Request) {
  log.Println("Executing finalHandler")
  w.Write([]byte("OK"))
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
  http.ListenAndServe(":3000", nil)
}
package main
 
import (
  "log"
  "net/http"
)
 
funcmiddlewareOne(nexthttp.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareOne")
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareOne again")
  })
}
 
funcmiddlewareTwo(nexthttp.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    log.Println("Executing middlewareTwo")
    if r.URL.Path != "/" {
      return
    }
    next.ServeHTTP(w, r)
    log.Println("Executing middlewareTwo again")
  })
}
 
funcfinal(w http.ResponseWriter, r *http.Request) {
  log.Println("Executing finalHandler")
  w.Write([]byte("OK"))
}
 
funcmain() {
  finalHandler := http.HandlerFunc(final)
 
  http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
  http.ListenAndServe(":3000", nil)
}

然后我們執行 go run main.go 在瀏覽器打開http://localhost:3000。 你會看到下面的輸出。

我們能夠很清楚的看到handle的流程控制。我們嵌套他們的返回順序。我們可以通過中間件中得 return 隨時停止handle鏈的控制。

在上面的代碼中我們在 middlewareTwo function包含了retrun 語句。我們在瀏覽器中打開http://localhost:3000/foo,我們會看到。

2015/12/19 04:21:57 Executing middlewareOne
2015/12/19 04:21:57 Executing middlewareTwo
2015/12/19 04:21:57 Executing middlewareOne again
2015/12/19 04:21:57 Executing middlewareOne
2015/12/19 04:21:57 Executing middlewareTwo
2015/12/19 04:21:57 Executing middlewareOne again
2015/12/19 04:21:57 ExecutingmiddlewareOne
2015/12/19 04:21:57 ExecutingmiddlewareTwo
2015/12/19 04:21:57 ExecutingmiddlewareOneagain
2015/12/19 04:21:57 ExecutingmiddlewareOne
2015/12/19 04:21:57 ExecutingmiddlewareTwo
2015/12/19 04:21:57 ExecutingmiddlewareOneagain

我們實現一個真實的項目的示例:

我們實現一個判斷請求是不是XMl的功能,我們要實現一個中間件。用來檢查的請求體的存在。檢查請求體,以確保它是XML。如果其中檢查失敗,我希望我們的中間件輸出錯誤信息然后終止我們的handle處理。

package main

import (
  "bytes"
  "net/http"
)

func enforceXMLHandler(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Check for a request body
    if r.ContentLength == 0 {
      http.Error(w, http.StatusText(400), 400)
      return
    }
    // Check its MIME type
    buf := new(bytes.Buffer)
    buf.ReadFrom(r.Body)
    if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
      http.Error(w, http.StatusText(415), 415)
      return
    }
    next.ServeHTTP(w, r)
  })
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", enforceXMLHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}
package main
 
import (
  "bytes"
  "net/http"
)
 
funcenforceXMLHandler(nexthttp.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Check for a request body
    if r.ContentLength == 0 {
      http.Error(w, http.StatusText(400), 400)
      return
    }
    // Check its MIME type
    buf := new(bytes.Buffer)
    buf.ReadFrom(r.Body)
    if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
      http.Error(w, http.StatusText(415), 415)
      return
    }
    next.ServeHTTP(w, r)
  })
}
 
funcmain() {
  finalHandler := http.HandlerFunc(final)
 
  http.Handle("/", enforceXMLHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}
 
funcfinal(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

為了檢驗我們的中間件是否實現了這個功能,我們首先創建一個XML文件。

$ cat > books.xml
<?xml version="1.0"?>
<books>
  <book>
    <author>H. G. Wells</author>
    <title>The Time Machine</title>
    <price>8.50</price>
  </book>
</books>
$ cat > books.xml
<?xmlversion="1.0"?>
<books>
  <book>
    <author>H. G. Wells</author>
    <title>TheTimeMachine</title>
    <price>8.50</price>
  </book>
</books>

然后通過使用cURL來進行模擬請求:

$ curl -i localhost:3000
HTTP/1.1 400 Bad Request
Content-Type: text/plain; charset=utf-8
Content-Length: 12

Bad Request
$ curl -i -d "This is not XML" localhost:3000
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/plain; charset=utf-8
Content-Length: 23

Unsupported Media Type
$ curl -i -d @books.xml localhost:3000
HTTP/1.1 200 OK
Date: Fri, 17 Oct 2014 13:42:10 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK
$ curl -i localhost:3000
HTTP/1.1 400 BadRequest
Content-Type: text/plain; charset=utf-8
Content-Length: 12
 
BadRequest
$ curl -i -d "This is not XML" localhost:3000
HTTP/1.1 415 UnsupportedMediaType
Content-Type: text/plain; charset=utf-8
Content-Length: 23
 
UnsupportedMediaType
$ curl -i -d @books.xmllocalhost:3000
HTTP/1.1 200 OK
Date: Fri, 17 Oct 2014 13:42:10 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
 
OK

接下來給大家介紹一下第三方中間件的使用:

秉承不造輪子的原則,其實在Github上有很多實現了一些功能的中間件。比如這里給大家介紹2個基礎驗證的中間件 goji/httpauth 和Gorilla’s  LoggingHandler

首先我們需要引入第三方包

$ go get github.com/goji/httpauth
$ gogetgithub.com/goji/httpauth
package main

import (
  "github.com/goji/httpauth"
  "net/http"
)

func main() {
  finalHandler := http.HandlerFunc(final)
  authHandler := httpauth.SimpleBasicAuth("username", "password")

  http.Handle("/", authHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}
package main
 
import (
  "github.com/goji/httpauth"
  "net/http"
)
 
funcmain() {
  finalHandler := http.HandlerFunc(final)
  authHandler := httpauth.SimpleBasicAuth("username", "password")
 
  http.Handle("/", authHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}
 
funcfinal(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

如果你運行這個例子,你應該得到你所期望的有效和無效的憑證響應

$ curl -i username:password@localhost:3000
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/plain; charset=utf-8

OK
$ curl -i username:wrongpassword@localhost:3000
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basic realm=""Restricted""
Content-Length: 13

Unauthorized
$ curl -i username:password@localhost:3000
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: text/plain; charset=utf-8
 
OK
$ curl -i username:wrongpassword@localhost:3000
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
Www-Authenticate: Basicrealm=""Restricted""
Content-Length: 13
 
Unauthorized

Gorilla’s LoggingHandler和 Apache-style logs 有一些區別

以下是我們在其中寫入日志到server.log文件一個簡單的例子:

首先還是引入第三包

go get github.com/gorilla/handlers
gogetgithub.com/gorilla/handlers
package main

import (
  "github.com/gorilla/handlers"
  "net/http"
  "os"
)

func main() {
  finalHandler := http.HandlerFunc(final)

  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }

  http.Handle("/", handlers.LoggingHandler(logFile, finalHandler))
  http.ListenAndServe(":3000", nil)
}

func final(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}
package main
 
import (
  "github.com/gorilla/handlers"
  "net/http"
  "os"
)
 
funcmain() {
  finalHandler := http.HandlerFunc(final)
 
  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }
 
  http.Handle("/", handlers.LoggingHandler(logFile, finalHandler))
  http.ListenAndServe(":3000", nil)
}
 
funcfinal(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("OK"))
}

在一個簡單的情況下,這樣我們的代碼是相當清楚的。但是,如果我們想用LoggingHandler作為一個更大的中間件鏈中的一部分會發生什么?我們可以很容易地結束了一個聲明,看起來像這樣:

http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))

不過這看起來太糟糕了。

我們可以通過創建一個構造函數打來整理一下我們給它取名為(myLoggingHandler)

和signature func(http.Handler) http.Handler .這樣就會是我們的代碼更加整潔和可讀性:

func myLoggingHandler(h http.Handler) http.Handler {
  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }
  return handlers.LoggingHandler(logFile, h)
}

func main() {
  finalHandler := http.HandlerFunc(final)

  http.Handle("/", myLoggingHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}
funcmyLoggingHandler(h http.Handler) http.Handler {
  logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
  if err != nil {
    panic(err)
  }
  return handlers.LoggingHandler(logFile, h)
}
 
funcmain() {
  finalHandler := http.HandlerFunc(final)
 
  http.Handle("/", myLoggingHandler(finalHandler))
  http.ListenAndServe(":3000", nil)
}
$ cat server.log
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2
$ catserver.log
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2
127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2

這里還有一個比較完整結構的中間件使用的示例:

package main

import (
    "bytes"
    "github.com/goji/httpauth"
    "github.com/gorilla/handlers"
    "net/http"
    "os"
)

func enforceXMLHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.ContentLength == 0 {
            http.Error(w, http.StatusText(400), 400)
            return
        }

        buf := new(bytes.Buffer)
        buf.ReadFrom(r.Body)
        if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
            http.Error(w, http.StatusText(415), 415)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func myLoggingHandler(h http.Handler) http.Handler {
    logFile, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    if err != nil {
        panic(err)
    }
    return handlers.LoggingHandler(logFile, h)
}

func main() {
    indexHandler := http.HandlerFunc(index)
    authHandler := httpauth.SimpleBasicAuth("username", "password")

    http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(indexHandler))))
    http.ListenAndServe(":3000", nil)
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("OK"))
}
package main
 
import (
 "bytes"
 "github.com/goji/httpauth"
 "github.com/gorilla/handlers"
 "net/http"
 "os"
)
 
funcenforceXMLHandler(nexthttp.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 if r.ContentLength == 0 {
 http.Error(w, http.StatusText(400), 400)
 return
 }
 
 buf := new(bytes.Buffer)
 buf.ReadFrom(r.Body)
 if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" {
 http.Error(w, http.StatusText(415), 415)
 return
 }
 
 next.ServeHTTP(w, r)
 })
}
 
funcmyLoggingHandler(h http.Handler) http.Handler {
 logFile, err := os.OpenFile("server.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
 if err != nil {
 panic(err)
 }
 return handlers.LoggingHandler(logFile, h)
}
 
funcmain() {
 indexHandler := http.HandlerFunc(index)
 authHandler := httpauth.SimpleBasicAuth("username", "password")
 
 http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(indexHandler))))
 http.ListenAndServe(":3000", nil)
}
 
funcindex(w http.ResponseWriter, r *http.Request) {
 w.Write([]byte("OK"))
}

有很多人不太喜歡中間件的設計模式,不過我還是慢喜歡的。

Go語言的Http 中間件實現

來自: https://xiequan.info/go語言的http-中間件實現/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!