https://mp.weixin.qq.com/s/-nRWwy8SjW1TlqCglL0CAQ
引言
web開發的背景下,“中間件”通常意思是“包裝原始應用並添加一些額外的功能的應用的一部分”。這個概念似乎總是不被人理解,但是我認為中間件非常棒。
首先,一個好的中間件有一個責任就是可插拔並且自足。這就意味着你可以在接口級別嵌入你的中間件他就能直接運行。它不會影響你編碼方式,不是框架,僅僅是你請求處理里面的一層而已。完全沒必要重寫你的代碼,如果你想使用中間件的一個功能,你就幫他插入到那里,如果不想使用了,就可以直接移除。
縱觀Go語言,中間件是非常普遍的,即使在標准庫中。雖然開始的時候不會那么明顯,在標准庫net/http中的函數StripText或者TimeoutHandler就是我們要定義和的中間件的樣子,處理請求和相應的時候他們包裝你的handler,並處理一些額外的步驟。
一開始,我們認為編寫中間件似乎很容易,但是我們實際編寫的時候也會遇到各種各樣的坑。讓我們來看看一些例子。
1、讀取請求
在我們的示例中,所有的中間件都將接受http。處理程序作為一個參數,並返回一個http.Handler。這使得人們很容易就能把中間產品串起來。我們所有的中間產品的基本模式是這樣的:
1
2
3
4
5
6
|
func X(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Something here...
h.ServeHTTP(w, r)
})
}
|
我們想要將所有的請求重定向到一個斜杠——比方說/message/,到它們的非斜杠等效,比如/message。我們可以這樣寫:
1
2
3
4
5
6
7
8
9
|
func TrailingSlashRedirect(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path[len(r.URL.Path)-1] == '/' {
http.Redirect(w, r, r.URL.Path[:len(r.URL.Path)-1], http.StatusMovedPermanently)
return
}
h.ServeHTTP(w, r)
})
}
|
有沒有很簡單。
2、修改請求
比方說,我們想要向請求添加一個標題,或者修改它。http.Handler文檔中指明:
除了讀取主體之外,處理程序不應該修改所提供的請求。
Go標准庫復制http.Request。請求對象在將其傳遞到響應鏈之前,我們也應該這樣做。假設我們想在每個請求上設置Request-Id頭,以便內部跟蹤。創建*Request的淺副本,並在代理之前修改標題。
1
2
3
4
5
6
7
8
|
func RequestID(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r2 := new(http.Request)
*r2 = *r
r2.Header.Set("X-Request-Id", uuid.NewV4().String())
h.ServeHTTP(w, r2)
})
}
|
3、編寫響應頭
如果你想設置響應頭,你可以只寫它們,然后代理請求。
1
2
3
4
5
6
|
func Server(h http.Handler, servername string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", servername)
h.ServeHTTP(w, r)
})
}
|
上面的問題是,如果內部處理器也設置了服務器頭,那么你的頭將被覆蓋。如果不想公開內部軟件的服務器頭,或者如果想在發送響應給客戶端之前去掉頭部,這可能會帶來問題。
要做到這一點,我們必須自己實現ResponseWriter接口。大多數時候,我們只會代理到底層的ResponseWriter,但是如果用戶試圖寫一個響應,我們就會潛入並添加我們的標題。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
type serverWriter struct {
w http.ResponseWriter
name string
wroteHeaders bool
}
func (s *serverWriter) Header() http.Header {
return s.w.Header()
}
func (s *serverWriter) WriteHeader(code int) http.Header {
if s.wroteHeader == false {
s.w.Header().Set("Server", s.name)
s.wroteHeader = true
}
s.w.WriteHeader(code)
}
func (s *serverWriter) Write(b []byte) (int, error) {
if s.wroteHeader == false {
// We hit this case if user never calls WriteHeader (default 200)
s.w.Header().Set("Server", s.name)
s.wroteHeader = true
} return s.w.Write(b)
}
|
要在我們的中間件中使用它,我們會寫:
1
2
3
4
5
6
7
8
9
|
func Server(h http.Handler, servername string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &serverWriter{
w: w,
name: servername,
}
h.ServeHTTP(sw, r)
})
}
|
問題
如果用戶從不調用Write或WriteHeader呢?例如,有一個200狀態並且是空body,或者對選項請求的響應——我們的攔截函數都不會運行。因此,我們應該在ServeHTTP調用之后再添加校驗。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func Server(h http.Handler, servername string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sw := &serverWriter{
w: w,
name: servername,
}
h.ServeHTTP(sw, r)
if sw.wroteHeaders == false {
s.w.Header().Set("Server", s.name)
s.wroteHeader = true
}
})
}
|
其他ResponseWriter接口
ResponseWriter接口只需要有三種方法。但在實踐中,它也可以對其他接口作出響應,例如http.Pusher。你的中間件可能會意外地禁用HTTP/2支持,這是不好的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// Push implements the http.Pusher interface.
func (s *serverWriter) Push(target string, opts *http.PushOptions) error {
if pusher, ok := s.w.(http.Pusher); ok {
return pusher.Push(target, opts)
}
return http.ErrNotSupported
}
// Flush implements the http.Flusher interface.
func (s *serverWriter) Flush() {
f, ok := s.w.(http.Flusher)
if ok {
f.Flush()
}
}
|
總結
通過以上的學習,不知道大家對Go編寫中間件有沒有一個完整的認識。大家也可以嘗試着用Go去編寫一個中間件。
好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
https://slarker.me/go-middleware/
今天學習了一個之前雖然聽過,但是從來沒搞懂的概念 — 中間件。這個概念在服務端開發中經常會用到,大的公司也會有專門的團隊來做中間件開發,用來提高應用層的開發效率。在 Web 開發中,中間件也就是在請求流程中新增了一層,添加一些額外的功能,比如驗證 Token,記錄日志等等。一個優秀的中間件能做到可插拔,自我約束,無須重寫代碼。
在 Go 語言中,實現一個中間件也非常簡單,其原理和 Python 中的裝飾器非常類似。比如我們想實現一個中間件,用來統計每個接口調用的次數:
package middleware import ( "fmt" "github.com/gin-gonic/gin" ) func RequestMiddleWare() gin.HandlerFunc { return func(c *gin.Context) { path := c.Request.URL.Path fmt.Println(string(path)) c.Next() } }
RequestMiddleWare
函數的返回值是一個函數,在函數實現中,可以根據 *gin.Context 拿到請求信息,做一些想要的操作,完成之后可以 c.Next() 執行下一個步驟。整個過程就是在原來的請求處理中新增了一層,非常方便。
除此之外,如果希望在請求結束后還可以做一些操作,可以把相關邏輯放到 c.Next() 之后,當請求結束之后,還會返回來再把中間件中剩余的邏輯執行完畢。也就相當於:
before middleware c.Next() after middleware
當構建 web 應用程序,可能對所有的請求會共享一些功能。例如我們每一個請求都會寫入日志。
- 打印 http 請求或返回的日志
- 壓縮 http 請求的返回值
- 將 http 請求頭保持一致
-
實現安全以及權限驗證
在 go 語言 net/http 標准庫中提供了中間件類似的函數 StripPrefix 和 TimeoutHandler。
如何自己編寫一個中間件呢?答案是
- 這個函數接收一個 http.Handler 作為參數,目的是可使用其他中間件或應用處理器,調用其他中間件或應用的方法就是調用其 HttpServe 的方法。
- 並且將 http.Handler 作為返回值,這樣其他中間件可以使用 http.Handler 作為輸入的參數,達到鏈式調用。
func middlewareHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 在執行應用處理器前編譯中間件的邏輯 next.ServeHTTP(w, r) // 在執行應用處理器后將會執行的編譯中間件的邏輯 }) }
從上面示例我們可以看出 go 語言是支持高階函數,通過向下傳遞 http.Handler 來實現中間件。
package main import ( "fmt" "log" "net/http" "time" ) func logginHandler(next http.Handler) http.Handler{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ start := time.Now() log.Printf("Started %s %s",r.Method, r.URL.Path) next.ServeHTTP(w,r) log.Printf("Completed %s in %v", r.URL.Path, time.Since(start)) }) } func index(w http.ResponseWriter,r *http.Request ){ log.Println("Executing index handler") fmt.Fprintf(w,"welcome!") } func about(w http.ResponseWriter, r *http.Request){ log.Println("Executing about handler") fmt.Fprintf(w,"Go Middleware") } func iconHandler(w http.ResponseWriter, r *http.Request){ } func main() { http.HandleFunc("/favicon.ico", iconHandler) indexHandler := http.HandlerFunc(index) aboutHandler := http.HandlerFunc(about) http.Handle("/",logginHandler(indexHandler)) http.Handle("/about",logginHandler(aboutHandler)) server := &http.Server{ Addr:":8080", } log.Println("Listening...") server.ListenAndServe() }
- 上面的代碼關鍵是 logginHandler 這個日志處理器,作為中間件會攔截請求,進行處理也就是輸出日志,通過
next.ServeHTTP(w,r)
然后將請求向下進行傳遞 - 這里創建兩個簡單路由處理器
index
和about
。都是向客戶端輸出不同的文本。 http.Handle("/",logginHandler(indexHandler))
通過 logginHandler 函數進行包裹,從容我們日志中間件可以攔截到發起的請求進行一些業務邏輯,業務邏輯可以位於next.ServeHTTP(w,r)
之前或之后。通過下面的輸出我們可以清晰可見代碼執行的順序。
Listening...
Started GET /
Executing index handler
Completed / in 41.129µs Started GET / Executing index handler Completed / in 50.475µs Started GET /about Executing about handler Completed /about in 49.483µs
許多書都提供代碼示例,個人建議是不要 copy 過來運行看效果,自己手 coding 就可以發現一些問題,同時可以思考為什么要這樣編寫代碼。
通過上面示例我們了解如何寫一個簡單中間件,這個個人看來和 nodejs 實現中間件沒有什么差別。實際開發中我們往往會有多個中間件來執行業務,那么這些中間件執行順序也是我們值得考慮的問題。
package main import( "fmt" "log" "net/http" ) func middlewareFirst(next http.Handler) http.Handler{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ log.Println("MiddlewareFirst - before Handler") next.ServeHTTP(w,r) log.Println("MiddlewareFirst - after Handler") }) } func middlewareSecond(next http.Handler) http.Handler{ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ log.Println("MiddlewareSecond - before Handler") if r.URL.Path == "/message"{ if r.URL.Query().Get("password") == "123"{ log.Println("Authorized to system...") next.ServeHTTP(w,r) }else{ log.Println("Failed to authorize to the system") return } }else{ next.ServeHTTP(w,r) } log.Println("MiddlewareScond - after Handler") }) } func index(w http.ResponseWriter, r *http.Request){ log.Println("Executed index Handler") fmt.Fprintf(w,"welcome") } func message(w http.ResponseWriter, r *http.Request){ log.Println("Executed message Handler") fmt.Fprintf(w,"message...") } func iconHandler(w http.ResponseWriter, r *http.Request){ } func main() { http.HandleFunc("/favicon",iconHandler) indexHandler := http.HandlerFunc(index) messageHandler := http.HandlerFunc(message) http.Handle("/",middlewareFirst(middlewareSecond(indexHandler))) http.Handle("/message",middlewareFirst(middlewareSecond(messageHandler))) server := &http.Server{ Addr:":8080", } log.Println("Listen...") server.ListenAndServe() }
在上面代碼中其實也沒有什么特別之處,就是我們創建了兩個middlewareFirst
和middlewareSecond
兩個中間件並且有一定先后順序,然后在 middlewareSecond
中寫了一個對訪問權限校驗的邏輯。大家可以嘗試地去運行一下。
https://segmentfault.com/a/1190000018819804
中間件是一種計算機軟件,可為操作系統提供的軟件應用程序提供服務,以便於各個軟件之間的溝通,特別是系統軟件和應用軟件。廣泛用於web應用和面向服務的體系結構等。
縱觀GO語言,中間件應用比較普遍,主要應用:
- 記錄對服務器發送的請求(request)
- 處理服務器響應(response )
- 請求和處理之間做一個權限認證工作
- 遠程調用
- 安全
- 等等
中間件處理程序是簡單的http.Handler
,它包裝另一個http.Handler
做請求的一些預處理和/或后處理。它被稱為“中間件”,因為它位於Go Web服務器和實際處理程序之間的中間位置。
下面是一些中間件例子
記錄日志中間件
package main import ( "fmt" "log" "net/http" ) func logging(f http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.Path) f(w, r) } } func foo(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "foo") } func bar(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "bar") } func main() { http.HandleFunc("/foo", logging(foo)) http.HandleFunc("/bar", logging(bar)) http.ListenAndServe(":8080", nil) }
訪問 http://localhost:8080/foo
返回結果
foo
將上面示例修改下,也可以實現相同的功能
package main import ( "fmt" "log" "net/http" ) func foo(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "foo") } func bar(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "bar") } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.Path) next.ServeHTTP(w, r) }) } func main() { http.Handle("/foo", loggingMiddleware(http.HandlerFunc(foo))) http.Handle("/bar", loggingMiddleware(http.HandlerFunc(bar))) http.ListenAndServe(":8080", nil) }
訪問 http://localhost:8080/foo
返回結果
foo
多中間件例子
package main import ( "fmt" "log" "net/http" "time" ) type Middleware func(http.HandlerFunc) http.HandlerFunc // Logging logs all requests with its path and the time it took to process func Logging() Middleware { // Create a new Middleware return func(f http.HandlerFunc) http.HandlerFunc { // Define the http.HandlerFunc return func(w http.ResponseWriter, r *http.Request) { // Do middleware things start := time.Now() defer func() { log.Println(r.URL.Path, time.Since(start)) }() // Call the next middleware/handler in chain f(w, r) } } } // Method ensures that url can only be requested with a specific method, else returns a 400 Bad Request func Method(m string) Middleware { // Create a new Middleware return func(f http.HandlerFunc) http.HandlerFunc { // Define the http.HandlerFunc return func(w http.ResponseWriter, r *http.Request) { // Do middleware things if r.Method != m { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } // Call the next middleware/handler in chain f(w, r) } } } // Chain applies middlewares to a http.HandlerFunc func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc { for _, m := range middlewares { f = m(f) } return f } func Hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello world") } func main() { http.HandleFunc("/", Chain(Hello, Method("GET"), Logging())) http.ListenAndServe(":8080", nil) }
中間件本身只是將其http.HandlerFunc
作為其參數之一,包裝它並返回一個新http.HandlerFunc
的服務器來調用。在這里,我們定義了一種新類型Middleware
,最終可以更容易地將多個中間件鏈接在一起。
當然我們也可以改成如下形式
package main import ( "fmt" "log" "net/http" "time" ) type Middleware func(http.Handler) http.Handler func Hello(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello world") } func Chain(f http.Handler, mmap ...Middleware) http.Handler { for _, m := range mmap { f = m(f) } return f } func Method(m string) Middleware { return func(f http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println(r.URL.Path) if r.Method != m { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } f.ServeHTTP(w, r) }) } } func Logging() Middleware { return func(f http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { //log.Println(r.URL.Path) // Do middleware things start := time.Now() defer func() { log.Println(r.URL.Path, time.Since(start)) }() f.ServeHTTP(w, r) }) } } func main() { http.Handle("/", Chain(http.HandlerFunc(Hello), Method("GET"), Logging())) http.ListenAndServe(":8080", nil) }
在gin框架下實現中間件
r := gin.Default() 創建帶有默認中間件的路由,默認是包含logger和recovery中間件的 r :=gin.new() 創建帶有沒有中間件的路由
示例
package main import ( "github.com/gin-gonic/gin" "log" "time" ) func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // Set example variable c.Set("example", "12345") // before request c.Next() // after request latency := time.Since(t) log.Print(latency) //時間 0s // access the status we are sending status := c.Writer.Status() log.Println(status) //狀態 200 } } func main() { r := gin.New() r.Use(Logger()) r.GET("/test", func(c *gin.Context) { example := c.MustGet("example").(string) // it would print: "12345" log.Println(example) }) // Listen and serve on 0.0.0.0:8080 r.Run(":8080") }
以上示例也可改為
package main import ( "github.com/gin-gonic/gin" "log" "time" ) func Logger() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() // Set example variable c.Set("example", "12345") // before request c.Next() // after request latency := time.Since(t) log.Print(latency) //時間 0s // access the status we are sending status := c.Writer.Status() log.Println(status) //狀態 200 } } func main() { r := gin.New() r.GET("/test", Logger(), func(c *gin.Context) { example := c.MustGet("example").(string) // it would print: "12345" log.Println(example) }) // Listen and serve on 0.0.0.0:8080 r.Run(":8080") }
即不用r.use添加中間件,直接將Logger() 寫到r.GET 方法的參數里("/test"之后)。
更多gin中間件示例可參考 https://github.com/gin-gonic/gin
在 Gin 中,接入中間件也非常簡單:
router := gin.Default() router.Use(middleware.RequestMiddleWare())
上面這段代碼是在所有的 API 中接入,如果僅僅想在某些 API 上接入,可以使用 Gin 的路由分組:
group := router.Group("/api/v1", middleware.RequestMiddleWare())