中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

go?gin如何正確讀取http?response?body內容并多次使用

發布時間:2023-01-09 09:29:20 來源:億速云 閱讀:145 作者:iii 欄目:開發技術

這篇文章主要介紹了go gin如何正確讀取http response body內容并多次使用的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇go gin如何正確讀取http response body內容并多次使用文章都會有所收獲,下面我們一起來看看吧。

    事件背景

    最近業務研發反映了一個需求:能不能讓現有基于 gin 的 webservice 框架能夠自己輸出 response 的信息,尤其是 response body 內容。因為研發在 QA 環境開發調試的時候,部署應用大多數都是 debug 模式,不想在每一個 http handler 函數中總是手寫一個日志去記錄 response body 內容,這樣做不但發布正式版本的時候要做清理,同時日常代碼維護也非常麻煩。如果 gin 的 webservice 框架能夠自己輸出 response 的信息到日志并記錄下來,這樣查看歷史應用運行狀態、相關請求信息和定位請求異常時也比較方便。

    針對這樣的需求,思考了下確實也是如此。平常自己寫服務的時候,本地調試用 Mock 數據各種沒有問題,但是一但進入到環境聯合調試的時候就各種問題,檢查服務接口在特定時間內出入參數也非常不方便。如果 webservice 框架能夠把 request 和 response 相關信息全量作為日志存在 Elasticsearch 中,也方便回溯和排查。

    要實現這個需求,用一個通用的 gin middleware 來做這個事情太合適了。并制作一個開關,匹配 GIN_MODE 這個環境變量,能夠在部署時候自動開關這個功能,可以極大減少研發的心智負擔。

    既然有這么多好處,說干就干。

    心智負擔

    通過對 gin 的代碼閱讀,發現原生 gin 框架沒有提供類似的功能,也說就要自己手寫一個。翻越了網上的解決方案,感覺都是淺淺說到了這個事情,但是沒有比較好的,且能夠應用工程中的。所以一不做二不休,自己整理一篇文章來詳細說明這個問題。我相信用 gin 作為 webservice 框架的小伙伴應該不少。

    說到這里,又要從原代碼看起來,那么產生 response 的地方在哪里? 當然是 http handler 函數。

    這里先舉個例子:

    func Demo(c *gin.Context) {
    	var r = []string{"lee", "demo"}
    	c.JSON(http.StatusOK, r)
    }

    這個函數返回內容為:["lee","demo"] 。但是為了要將這個請求的 request 和 response 內容記錄到日志中,就需要編寫類似如下的代碼。

    func Demo(c *gin.Context) {
    	var r = []string{"lee", "demo"}
    	c.JSON(http.StatusOK, r)
    	// 記錄相關的內容
    	b, _ := json.Marshal(r)
    	log.Println("request: ", c.Request)
    	log.Println("resposeBody: ", b)
    }

    各位小伙伴,嘗試想想每一個 http handler 函數都要你寫一遍,然后要針對運行環境是 QA 還是 Online 做判斷,或者在發布 Online 時候做代碼清理。我想研發小伙伴都會說:NO!! NO!! NO!!

    前置知識

    最好的辦法是將這個負擔交給 gin 的 webservice 框架來處理,研發不需要做相關的邏輯。居然要這么做,那么就要看看 gin 的 response 是怎么產生的。

    用上面提到的 c.JSON 方法來舉例。

    github.com/gin-gonic/gin@v1.8.1/context.go

    // JSON serializes the given struct as JSON into the response body.
    // It also sets the Content-Type as "application/json".
    func (c *Context) JSON(code int, obj any) {
    	c.Render(code, render.JSON{Data: obj})
    }

    這個 c.JSON 實際是 c.Render 的一個包裝函數,繼續往下追。

    github.com/gin-gonic/gin@v1.8.1/context.go

    // Render writes the response headers and calls render.Render to render data.
    func (c *Context) Render(code int, r render.Render) {
    	c.Status(code)
    	if !bodyAllowedForStatus(code) {
    		r.WriteContentType(c.Writer)
    		c.Writer.WriteHeaderNow()
    		return
    	}
    	if err := r.Render(c.Writer); err != nil {
    		panic(err)
    	}
    }

    c.Render 還是一個包裝函數,最終是用 r.Render 向 c.Writer 輸出數據。

    github.com/gin-gonic/gin@v1.8.1/render/render.go

    // Render interface is to be implemented by JSON, XML, HTML, YAML and so on.
    type Render interface {
    	// Render writes data with custom ContentType.
    	Render(http.ResponseWriter) error
    	// WriteContentType writes custom ContentType.
    	WriteContentType(w http.ResponseWriter)
    }

    r.Render 是一個渲染接口,也就是 gin 可以輸出 JSON,XML,String 等等統一接口。 此時我們需要找 JSON 實現體的相關信息。

    github.com/gin-gonic/gin@v1.8.1/render/json.go

    // Render (JSON) writes data with custom ContentType.
    func (r JSON) Render(w http.ResponseWriter) (err error) {
    	if err = WriteJSON(w, r.Data); err != nil {
    		panic(err)
    	}
    	return
    }
    // WriteJSON marshals the given interface object and writes it with custom ContentType.
    func WriteJSON(w http.ResponseWriter, obj any) error {
    	writeContentType(w, jsonContentType)
    	jsonBytes, err := json.Marshal(obj)
    	if err != nil {
    		return err
    	}
    	_, err = w.Write(jsonBytes) // 寫入 response 內容,內容已經被 json 序列化
    	return err
    }

    追到這里,真正輸出內容的函數是 WriteJSON,此時調用 w.Write(jsonBytes) 寫入被 json 模塊序列化完畢的對象。而這個 w.Write 是 http.ResponseWriter 的方法。那我們就看看 http.ResponseWriter 到底是一個什么樣子的?

    net/http/server.go

    // A ResponseWriter may not be used after the Handler.ServeHTTP method
    // has returned.
    type ResponseWriter interface {
    	...
    	// Write writes the data to the connection as part of an HTTP reply.
    	//
    	// If WriteHeader has not yet been called, Write calls
    	// WriteHeader(http.StatusOK) before writing the data. If the Header
    	// does not contain a Content-Type line, Write adds a Content-Type set
    	// to the result of passing the initial 512 bytes of written data to
    	// DetectContentType. Additionally, if the total size of all written
    	// data is under a few KB and there are no Flush calls, the
    	// Content-Length header is added automatically.
    	//
    	// Depending on the HTTP protocol version and the client, calling
    	// Write or WriteHeader may prevent future reads on the
    	// Request.Body. For HTTP/1.x requests, handlers should read any
    	// needed request body data before writing the response. Once the
    	// headers have been flushed (due to either an explicit Flusher.Flush
    	// call or writing enough data to trigger a flush), the request body
    	// may be unavailable. For HTTP/2 requests, the Go HTTP server permits
    	// handlers to continue to read the request body while concurrently
    	// writing the response. However, such behavior may not be supported
    	// by all HTTP/2 clients. Handlers should read before writing if
    	// possible to maximize compatibility.
    	Write([]byte) (int, error)
    	...
    }

    哦喲,最后還是回到了 golang 自己的 net/http 包了,看到 ResponseWriter 是一個 interface。那就好辦了,就不怕你是一個接口,我只要對應的實現體給你不就能解決問題了嗎?好多人都是這么想的。

    說得輕巧,這里有好幾個問題在面前:

    • 什么樣的 ResponseWriter 實現才能解決問題?

    • 什么時候傳入新的 ResponseWriter 覆蓋原有的 ResponseWriter 對象?

    • 怎樣做代價最小,能夠減少對原有邏輯的入侵。能不能做到 100% 兼容原有邏輯?

    • 怎么做才是最高效的做法,雖然是 debug 環境,但是 QA 環境不代表沒有流量壓力

    解決思路

    帶著上章中的問題,要真正的解決問題,就需要回到 gin 的框架結構中去尋找答案。

    追本溯源

    gin 框架中的 middleware 實際是一個鏈條,并按照 Next() 的調用順序逐一往下執行。

    go?gin如何正確讀取http?response?body內容并多次使用

    Next() 與執行順序

    go?gin如何正確讀取http?response?body內容并多次使用

    middleware 執行的順序會從最前面的 middleware 開始執行,在 middleware function 中,一旦執行 Next() 方法后,就會往下一個 middleware 的 function 走,但這并不表示 Next() 后的內容不會被執行到,相反的,Next()后面的內容會等到所有 middleware function 中 Next() 以前的程式碼都執行結束后,才開始執行,并且由后往前且逐一完成。

    舉個例子,方便小伙伴理解:

    func main() {
    	router := gin.Default()
    	router.GET("/api", func(c *gin.Context) {
    		fmt.Println("First Middle Before Next")
    		c.Next()
    		fmt.Println("First Middle After Next")
    	}, func(c *gin.Context) {
    		fmt.Println("Second Middle Before Next")
    		c.Next()
    		fmt.Println("Second Middle After Next")
    	}, func(c *gin.Context) {
    		fmt.Println("Third Middle Before Next")
    		c.Next()
    		fmt.Println("Third Middle After Next")
    		c.JSON(http.StatusOK, gin.H{
    			"message": "pong",
    		})
    	})
    }

    Console 執行結果如下:

    // Next 之前的內容會「由前往后」並且「依序」完成
    First Middle Before Next
    Second Middle Before Next
    Third Middle Before Next

    // Next 之后的內容會「由后往前」並且「依序」完成
    Third Middle After Next
    Second Middle After Next
    First Middle After Next

    通過上面的例子,我們看到了 gin 框架中的 middleware 中處理流程。為了讓 gin 的 webservice 框架在后續的 middleware 中都能輕松獲得 func(c *gin.Context) 產生的 { "message": "pong" }, 就要結合上一章找到的 WriteJSON 函數,讓其輸出到 ResponseWriter 的內容保存到 gin 的 Context 中 (gin 框架中,每一個 http 回話都與一個 Context 對象綁定),這樣就可以在隨后的 middleware 能夠輕松訪問到 response body 中的內容。

    上手開發

    還是回到上一章中的 4 個核心問題,我想到這里應該有答案了:

    • 構建一個自定義的 ResponseWriter 實現,覆蓋原有的 net/http 框架中 ResponseWriter,并實現對數據存儲。 -- 回答問題 1

    • 攔截 c.JSON 底層 WriteJSON 函數中的 w.Write 方法,就可以對框架無損。 -- 回答問題 2,3

    • 在 gin.Use() 函數做一個開關,當 GIN_MODE 是 release 模式,就不注入這個 middleware,這樣第 1,2 就不會存在,而是原有的 net/http 框架中 ResponseWriter -- 回答問題 3,4

    說到了這么多內容,我們來點實際的。

    第 1 點代碼怎么寫

    type responseBodyWriter struct {
    	gin.ResponseWriter  // 繼承原有 gin.ResponseWriter
    	bodyBuf *bytes.Buffer  // Body 內容臨時存儲位置,這里指針,原因這個存儲對象要復用
    }
    // 覆蓋原有 gin.ResponseWriter 中的 Write 方法
    func (w *responseBodyWriter) Write(b []byte) (int, error) {
    	if count, err := w.bodyBuf.Write(b); err != nil {  // 寫入數據時,也寫入一份數據到緩存中
    		return count, err
    	}
    	return w.ResponseWriter.Write(b) // 原始框架數據寫入
    }

    第 2 點代碼怎么寫

    創建一個 bytes.Buffer 指針 pool

    type bodyBuff struct {
    	bodyBuf *bytes.Buffer
    }
    func newBodyBuff() *bodyBuff {
    	return &bodyBuff{
    		bodyBuf: bytes.NewBuffer(make([]byte, 0, bytesBuff.ConstDefaultBufferSize)),
    	}
    }
    var responseBodyBufferPool = sync.Pool{New: func() interface{} {
    	return newBodyBuff()
    }}

    創建一個 gin middleware,用于從 pool 獲得 bytes.Buffer 指針,并創建 responseBodyWriter 對象覆蓋原有 gin 框架中 Context 中的 ResponseWriter,隨后清理對象回收 bytes.Buffer 指針到 pool 中。

    func ginResponseBodyBuffer() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		var b *bodyBuff
    		// 創建緩存對象
    		b = responseBodyBufferPool.Get().(*bodyBuff)
    		b.bodyBuf.Reset()
    		c.Set(responseBodyBufferKey, b)
    		// 覆蓋原有 writer
    		wr := responseBodyWriter{
    			ResponseWriter: c.Writer,
    			bodyBuf:        b.bodyBuf,
    		}
    		c.Writer = &wr
    		// 下一個
    		c.Next()
    		// 歸還緩存對象
    		wr.bodyBuf = nil
    		if o, ok := c.Get(responseBodyBufferKey); ok {
    			b = o.(*bodyBuff)
    			b.bodyBuf.Reset()
    			responseBodyBufferPool.Put(o)     // 歸還對象
    			c.Set(responseBodyBufferKey, nil) // 釋放指向 bodyBuff 對象
    		}
    	}
    }

    第 3 點代碼怎么寫

    這里最簡單了,寫一個 if 判斷就行了。

    func NewEngine(...) *Engine {
    	...
    	engine := new(Engine)
    	...
    	if gin.IsDebugging() {
    		engine.ginSvr.Use(ginResponseBodyBuffer())
    	}
    	...
    }

    看到這里,有的小伙伴就會問了, 你還是沒有說怎么輸出啊,我抄不到作業呢。也是哦,都說到這里了,感覺現在不給作業抄,怕是有小伙伴要掀桌子。

    這次“作業”的整體思路是:ginResponseBodyBuffer 在 Context 中 創建 bodyBuf,然后由其他的 middleware 函數處理,最終在處理函數中生成 http response,通過攔截 c.JSON 底層 WriteJSON 函數中的 w.Write 方法,記錄http response body 到之前 ginResponseBodyBuffer 生成的 bodyBuf 中。最后數據到 ginLogger 中輸出生成日志,將 http response body 輸出保存相,之后由 ginResponseBodyBuffer 回收資源。

    作業 1:日志輸出 middleware 代碼編寫

    func GenerateResponseBody(c *gin.Context) string {
    	if o, ok := c.Get(responseBodyBufferKey); ok {
    		return utils.BytesToString(o.(*bodyBuff).bodyBuf.Bytes())
    	} else {
    		return "failed to get response body"
    	}
    }
    func ginLogger() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		// 正常處理系統日志
    		path := GenerateRequestPath(c)
    		requestBody := GenerateRequestBody(c)
    		// 下一個
    		c.Next()
    		// response 返回
    		responseBody := GenerateResponseBody(c)
    		// 日志輸出
    		log.Println("path: ", path, "requestBody: ", requestBody, "responseBody", responseBody)
    	}
    }

    作業 2:日志輸出 middleware 安裝

    func NewEngine(...) *Engine {
    	...
    	engine := new(Engine)
    	...
    	if gin.IsDebugging() {
    		engine.ginSvr.Use(ginResponseBodyBuffer(), ginLogger())
    	}
    	...
    }

    這里只要把 ginLogger 放在 ginResponseBodyBuffer 這個 middleware 后面就可以了。

    測試代碼

    go?gin如何正確讀取http?response?body內容并多次使用

    Console 內容輸出

    $ curl -i http://127.0.0.1:8080/xx/
    HTTP/1.1 200 OK
    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Headers: Content-Type, AccessToken, X-CSRF-Token, Authorization, Token
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
    Access-Control-Allow-Origin: *
    Access-Control-Expose-Headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
    Content-Type: application/json; charset=utf-8
    X-Request-Id: 1611289702609555456
    Date: Fri, 06 Jan 2023 09:12:56 GMT
    Content-Length: 14
    ["lee","demo"]

    服務日志輸出

    {"level":"INFO","time":"2023-01-06T17:12:56.074+0800","caller":"server/middleware.go:78","message":"http access log","requestID":"1611289702609555456","status":200,"method":"GET","contentType":"","clientIP":"127.0.0.1","clientEndpoint":"127.0.0.1:62865","path":"/xx/","latency":"280.73µs","userAgent":"curl/7.54.0","requestQuery":"","requestBody":"","responseBody":"[\"lee\",\"demo\"]"}

    關于“go gin如何正確讀取http response body內容并多次使用”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“go gin如何正確讀取http response body內容并多次使用”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    大厂| 正安县| 洮南市| 雷山县| 镇平县| 新干县| 伊宁市| 平原县| 安泽县| 长葛市| 尉犁县| 庆安县| 宜昌市| 精河县| 阿拉尔市| 金山区| 上饶市| 郴州市| 阳江市| 台中县| 林西县| 北安市| 龙胜| 贡山| 贡嘎县| 布拖县| 河南省| 肇源县| 沁水县| 永德县| 建宁县| 军事| 台山市| 澎湖县| 东丰县| 行唐县| 周至县| 集贤县| 岑溪市| 伽师县| 长阳|