golang程序設計:Go middleware中間件以及Gin 中間件分析


先從業務開發角度出發,來逐漸引出中間件。

一、剛開始時業務開發

開始業務開發時,業務需求比較少。

  1. 當我們最開始進行業務開發時,需求不是很多。 第一個需求產是品向大家打聲招呼:“hello world”。

接到需求任務,我們就進行代碼開發了。
一般都會寫下如下的代碼,用handlefunc來處理請求的服務

package main

import (
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

func main() {
	http.HandleFunc("/", helloHandler)
	http.ListenAndServe(":8080", nil)
}

  1. 假如現在業務有變化了,我們要新增一個hello服務的處理耗時,怎么做?
    這個需求比較簡單,修改代碼如下:
package main

import (
	"log"
	"net/http"
	"os"
	"time"
)

var logger = log.New(os.Stdout, "", 0)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	timeStart := time.Now()
	w.Write([]byte("hello world"))
	timeElapsed := time.Since(timeStart)
	logger.Println(timeElapsed)
}

func main() {
	http.HandleFunc("/", helloHandler)
	http.ListenAndServe(":8080", nil)
}

這樣就可以輸出當前hello請求到日志文件了。

  1. 完成了這個需求后。過了沒多久,又有新的需求來了,我們要顯示信息,顯示Email,
    顯示好朋友,並且這就一個接口也需要增加耗時記錄。

一下子又增加了很多api, 簡略示例代碼如下:


package main

func helloHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}

func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}

func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}

func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("your friends is tom and alex"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.HandleFunc("/info/show", showInfoHandler)
    http.HandleFunc("/email/show", showEmailHandler)
    http.HandleFunc("/friends/show", showFriendsHandler)
    // ...
}

每一個handler里面都需要記錄運行的時間,每次新加路由都要寫同樣的代碼。都要把業務邏輯代碼拷貝過來。

  1. 業務繼續發展,又有了新的需求,增加一個監控系統,需要你上報接口運行時間到監控系統里面,以便監控接口的穩定性。這個監控系統叫metrics。

好了,現在你又要修改代碼,通過http post方式把耗時時間發送給metrics系統。
而且你要修改好多個handler,增加metrics上報接口代碼。

修改代碼:

func helloHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
    // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}

func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
    
    // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}

func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
    
     // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}

func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("your friends is tom and alex"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
    
     // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}

到這里,發現要修改好多的handler函數,才能把接口的耗時時間上報到metrics系統里。
隨着新需求越來越多,handler也會越多,那么我們修改的地方也就越多。增加了一個簡單的業務統計,就要修改好多個handler函數。

雖然只是增加一個業務統計,我們就要去修改handler,來增加這些和業務無關的代碼。
一開始我們並沒有做錯什么, 但是隨着業務的發展,我們逐漸陷入了代碼的泥潭。

接下來,我們該怎么辦呢?怎么處理這種情況?

二、業務逐漸多了后

隨着業務發展,handler越來越多,增加與業務無關的代碼所要修改的地方也越來越多。這時候怎么辦?有沒有辦法可以處理這種情況呢?

想一下,java里面有一個Filter的技術,可以攔截請求的處理。我們可不可以利用這個思想來解決我們的問題呢。這種思想當然是可以的。

在go里面就是利用 http.Handler 來把你要處理的函數包起來(實際就是攔截了),然后處理。
在go里面有一個學名叫 middleware中間件),中間件常見的位置是ServeMux和應用處理程序之間。

http的請求控制流程:

ServeMux => Middleware Handler => Application Handler

針對上面的需求,我們用中間件的方法來修改下:

package main

import (
	"log"
	"net/http"
	"os"
	"time"
)

var logger = log.New(os.Stdout, "", 0)

func hello(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("hello world"))
}

func timeMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		timeStart := time.Now()

		//next handler
		next.ServeHTTP(w, r)

		timeElapsed := time.Since(timeStart)
		logger.Println(timeElapsed)
	})
}

func main() {
	http.HandleFunc("/", timeMiddleWare(hello))
	http.ListenAndServe(":8080", nil)
}

這樣就實現了中間件。

也是把業務代碼和非業務代碼進行了剝離。

三、怎么實現中間件

上面的中間件是怎么實現的呢?

  1. 他要滿足http.Handler 接口
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

寫一個簡單的程序:

func showinfoHandler(info string) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte(info)
  })
}

上面程序我們把簡單的處理程序w.Write放在了一個匿名函數中,然后引用了外部的info構成了一個閉包。接下來我們用 http.HandlerFunc 適配器將此閉包轉換為處理程序,然后返回它。

我們可以用相同的方法,將下一個處理程序作為變量來進行傳遞,然后調用ServeHTTP() 方法將控制轉移到下一個處理程序,然后返回它。

func demoMiddleware(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // out logic
        
        next.ServeHTTP(w, r)
  })
}

上面的中間件函數有一個 func(http.Handler) http.Handler 的函數簽名。它接收一個處理程序作為參數並返回另外一個處理程序。

一個完整的例子

用一個完整的例子來看看中間件的執行流程:
middlewaredemo.go

package main

import (
	"log"
	"net/http"
)

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

func middleTwo(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Println("before middletwo")
		if r.URL.Path == "/foo" {
			return
		}
		next.ServeHTTP(w, r)
		log.Println("after middleTwo again")
	})
}

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

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

	http.Handle("/", middleOne(middleTwo(finalHandler)))
	http.ListenAndServe(":3030", nil)
}

先執行下面的命令

go run middlewaredemo.go

然后在瀏覽器上執行:http://localhost:3030/
運行結果:

2020/04/19 23:07:14 before middleone
2020/04/19 23:07:14 before middletwo
2020/04/19 23:07:14 exec finalHandler
2020/04/19 23:07:14 after middleTwo again
2020/04/19 23:07:14 after middlewareOne again

看看執行結果,在 next.ServeHTTP ,看到處理順序是依次按照嵌套的順序出結果, 但是在 next.ServeHTTP 的程序,是按照相反的方向出結果。

然后在運行 : http://localhost:3030/foo
運行結果:

2020/04/19 23:12:15 before middleone
2020/04/19 23:12:15 before middletwo
2020/04/19 23:12:15 after middlewareOne again

middleTwo 函數里 return 后面的程序,都沒有顯示了。
所以,在中間件中,我們可以用 return 來停止在中間件程序的傳播。

四、Gin框架的中間件

github.com/gin-gonic/gin,這個web框架使用很廣泛,它也有中間件功能。

  • 使用方法 一
// 定義中間件
func middlewareOne(c *gin.Context) {
    // 中間件邏輯
}
// 使用中間件
r := gin.Default()
r.Use(middlewareOne)
  • 使用方法 二
func middlewareTwo() gin.HandlerFunc {
    // 自定義邏輯
    return func(c *gin.Context) {
        // 中間件邏輯
    }
}
// 使用中間件
r := gin.Deafult()
r.Use(middlewareTwo()

Gin還有一種像java中的Filter,處理前,處理后的一種方法 Next()
比如:demo1.go

package main

import (
	"fmt"
	"time"
	"github.com/gin-gonic/gin"
)

func middlewareOne() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.String(200, "before middlewareOne: handler "+time.Now().String()+"\n")

		c.Next()

		c.String(200, "after middlewareOne : handler "+time.Now().String()+"\n")
	}
}

func middlewareTwo() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.String(200, "before middlewareTwo: handler \n")

		c.Next()

		c.String(200, "after middlewareTwo : handler \n")
	}
}

func main() {
	r := gin.Default()

	r.Use(middlewareOne(), middlewareTwo())

	r.GET("/", func(c *gin.Context) {
		fmt.Println("start!")
		c.String(200, "GET method \n")
		fmt.Println("end!")
	})

	r.Run(":8080")
}

先在命令行運行:

go run demo1.go

然后在瀏覽器上輸入:http://localhost:8080/
就可以看到輸出結果:

before middlewareOne: handler 2020-04-20 01:03:25.4547842 +0800 CST m=+21.318612501
before middlewareTwo: handler
GET method
after middlewareTwo : handler
after middlewareOne : handler 2020-04-20 01:03:25.4547842 +0800 CST m=+21.318612501

其實我們看到gin框架實現的中間件,它書寫形式並不是像上面的那種嵌套模式。如果有很多中間件的話,那么這種嵌套模式寫出來就讓人感覺非常復雜。

而gin中間件這種書寫模式,就很清晰,適合人閱讀。是一種優雅的實現方式。

它是怎么實現的呢?

五、Gin中間件的實現

gin v1.6.2 版本

gin.go

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array. 
//定義了Handlers鏈
type HandlersChain []HandlerFunc

// Last returns the last handler in the chain. ie. the last handler is the main one.
// 從Handlers里取出最后一個Handler,就是main自己
func (c HandlersChain) Last() HandlerFunc {
    if length := len(c); length > 0 {
        return c[length-1]
    }
    return nil
}

// RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct {
    Method      string
    Path        string
    Handler     string
    HandlerFunc HandlerFunc
}
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo

type Engine struct {
    RouterGroup
    // Enables automatic redirection if the current route can't be matched but a
    // handler for the path with (without) the trailing slash exists.
    // For example if /foo/ is requested but a route only exists for /foo, the
    // client is redirected to /foo with http status code 301 for GET requests
    // and 307 for all other request methods.
    RedirectTrailingSlash bool

    ... ...
}

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
// 初始化Engine,里面就用到了2個中間件函數Logger和Recovery
func Default() *Engine {
    debugPrintWARNINGDefault()
    engine := New()
    engine.Use(Logger(), Recovery())
    return engine
}

// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
// 增加 middleware -> 實質是到 RouterGroup.Use()
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)  //到了 RouterGroup 里的Use
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

context.go


// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter
    Params   Params
    handlers HandlersChain   //這里有一個handlers 鏈,一個slice
    index    int8
    fullPath string
    engine *Engine
    // This mutex protect Keys map
    KeysMutex *sync.RWMutex
    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}
  
   ... ...
}

// Handler returns the main handler.
// 返回main handler
func (c *Context) Handler() HandlerFunc {
    return c.handlers.Last()
}

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

還有一個routegroup.go里的handler chain

// Use adds middleware to the group, see example code in GitHub.
// 增加middleware
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
    finalSize := len(group.Handlers) + len(handlers)
    if finalSize >= int(abortIndex) {
        panic("too many handlers")
    }
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}

六、改進以前框架

寫到了這里,想起了以前寫的 golang web框架 文章,那只是實現了一個簡單的MVC功能,並不具備可擴展性,有了這個中間件技術,就可以把以前的框架進行改進。
改進后的全新 go web 框架 lilac

先實現功能,然后再進行優化改進 - 論開發。

七、參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM