go?gin?正確讀取http?response?body內(nèi)容并多次使用詳解
事件背景
最近業(yè)務(wù)研發(fā)反映了一個(gè)需求:能不能讓現(xiàn)有基于 gin 的 webservice 框架能夠自己輸出 response 的信息,尤其是 response body 內(nèi)容。因?yàn)檠邪l(fā)在 QA 環(huán)境開發(fā)調(diào)試的時(shí)候,部署應(yīng)用大多數(shù)都是 debug 模式,不想在每一個(gè) http handler 函數(shù)中總是手寫一個(gè)日志去記錄 response body 內(nèi)容,這樣做不但發(fā)布正式版本的時(shí)候要做清理,同時(shí)日常代碼維護(hù)也非常麻煩。如果 gin 的 webservice 框架能夠自己輸出 response 的信息到日志并記錄下來(lái),這樣查看歷史應(yīng)用運(yùn)行狀態(tài)、相關(guān)請(qǐng)求信息和定位請(qǐng)求異常時(shí)也比較方便。
針對(duì)這樣的需求,思考了下確實(shí)也是如此。平常自己寫服務(wù)的時(shí)候,本地調(diào)試用 Mock 數(shù)據(jù)各種沒(méi)有問(wèn)題,但是一但進(jìn)入到環(huán)境聯(lián)合調(diào)試的時(shí)候就各種問(wèn)題,檢查服務(wù)接口在特定時(shí)間內(nèi)出入?yún)?shù)也非常不方便。如果 webservice 框架能夠把 request 和 response 相關(guān)信息全量作為日志存在 Elasticsearch 中,也方便回溯和排查。
要實(shí)現(xiàn)這個(gè)需求,用一個(gè)通用的 gin middleware 來(lái)做這個(gè)事情太合適了。并制作一個(gè)開關(guān),匹配 GIN_MODE 這個(gè)環(huán)境變量,能夠在部署時(shí)候自動(dòng)開關(guān)這個(gè)功能,可以極大減少研發(fā)的心智負(fù)擔(dān)。
既然有這么多好處,說(shuō)干就干。
心智負(fù)擔(dān)
通過(guò)對(duì) gin 的代碼閱讀,發(fā)現(xiàn)原生 gin 框架沒(méi)有提供類似的功能,也說(shuō)就要自己手寫一個(gè)。翻越了網(wǎng)上的解決方案,感覺(jué)都是淺淺說(shuō)到了這個(gè)事情,但是沒(méi)有比較好的,且能夠應(yīng)用工程中的。所以一不做二不休,自己整理一篇文章來(lái)詳細(xì)說(shuō)明這個(gè)問(wèn)題。我相信用 gin 作為 webservice 框架的小伙伴應(yīng)該不少。
說(shuō)到這里,又要從原代碼看起來(lái),那么產(chǎn)生 response 的地方在哪里? 當(dāng)然是 http handler 函數(shù)。
這里先舉個(gè)例子:
func Demo(c *gin.Context) {
var r = []string{"lee", "demo"}
c.JSON(http.StatusOK, r)
}
這個(gè)函數(shù)返回內(nèi)容為:["lee","demo"] 。但是為了要將這個(gè)請(qǐng)求的 request 和 response 內(nèi)容記錄到日志中,就需要編寫類似如下的代碼。
func Demo(c *gin.Context) {
var r = []string{"lee", "demo"}
c.JSON(http.StatusOK, r)
// 記錄相關(guān)的內(nèi)容
b, _ := json.Marshal(r)
log.Println("request: ", c.Request)
log.Println("resposeBody: ", b)
}
各位小伙伴,嘗試想想每一個(gè) http handler 函數(shù)都要你寫一遍,然后要針對(duì)運(yùn)行環(huán)境是 QA 還是 Online 做判斷,或者在發(fā)布 Online 時(shí)候做代碼清理。我想研發(fā)小伙伴都會(huì)說(shuō):NO!! NO!! NO!!
前置知識(shí)
最好的辦法是將這個(gè)負(fù)擔(dān)交給 gin 的 webservice 框架來(lái)處理,研發(fā)不需要做相關(guān)的邏輯。居然要這么做,那么就要看看 gin 的 response 是怎么產(chǎn)生的。
用上面提到的 c.JSON 方法來(lái)舉例。
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})
}
這個(gè) c.JSON 實(shí)際是 c.Render 的一個(gè)包裝函數(shù),繼續(xù)往下追。
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 還是一個(gè)包裝函數(shù),最終是用 r.Render 向 c.Writer 輸出數(shù)據(jù)。
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 是一個(gè)渲染接口,也就是 gin 可以輸出 JSON,XML,String 等等統(tǒng)一接口。 此時(shí)我們需要找 JSON 實(shí)現(xiàn)體的相關(guān)信息。
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 內(nèi)容,內(nèi)容已經(jīng)被 json 序列化
return err
}
追到這里,真正輸出內(nèi)容的函數(shù)是 WriteJSON,此時(shí)調(diào)用 w.Write(jsonBytes) 寫入被 json 模塊序列化完畢的對(duì)象。而這個(gè) w.Write 是 http.ResponseWriter 的方法。那我們就看看 http.ResponseWriter 到底是一個(gè)什么樣子的?
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 是一個(gè) interface。那就好辦了,就不怕你是一個(gè)接口,我只要對(duì)應(yīng)的實(shí)現(xiàn)體給你不就能解決問(wèn)題了嗎?好多人都是這么想的。
說(shuō)得輕巧,這里有好幾個(gè)問(wèn)題在面前:
- 什么樣的 ResponseWriter 實(shí)現(xiàn)才能解決問(wèn)題?
- 什么時(shí)候傳入新的 ResponseWriter 覆蓋原有的 ResponseWriter 對(duì)象?
- 怎樣做代價(jià)最小,能夠減少對(duì)原有邏輯的入侵。能不能做到 100% 兼容原有邏輯?
- 怎么做才是最高效的做法,雖然是 debug 環(huán)境,但是 QA 環(huán)境不代表沒(méi)有流量壓力
解決思路
帶著上章中的問(wèn)題,要真正的解決問(wèn)題,就需要回到 gin 的框架結(jié)構(gòu)中去尋找答案。
追本溯源
gin 框架中的 middleware 實(shí)際是一個(gè)鏈條,并按照 Next() 的調(diào)用順序逐一往下執(zhí)行。

Next() 與執(zhí)行順序

middleware 執(zhí)行的順序會(huì)從最前面的 middleware 開始執(zhí)行,在 middleware function 中,一旦執(zhí)行 Next() 方法后,就會(huì)往下一個(gè) middleware 的 function 走,但這并不表示 Next() 后的內(nèi)容不會(huì)被執(zhí)行到,相反的,Next()后面的內(nèi)容會(huì)等到所有 middleware function 中 Next() 以前的程式碼都執(zhí)行結(jié)束后,才開始執(zhí)行,并且由后往前且逐一完成。
舉個(gè)例子,方便小伙伴理解:
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 執(zhí)行結(jié)果如下:
// Next 之前的內(nèi)容會(huì)「由前往后」並且「依序」完成
First Middle Before Next
Second Middle Before Next
Third Middle Before Next// Next 之后的內(nèi)容會(huì)「由后往前」並且「依序」完成
Third Middle After Next
Second Middle After Next
First Middle After Next
通過(guò)上面的例子,我們看到了 gin 框架中的 middleware 中處理流程。為了讓 gin 的 webservice 框架在后續(xù)的 middleware 中都能輕松獲得 func(c *gin.Context) 產(chǎn)生的 { "message": "pong" }, 就要結(jié)合上一章找到的 WriteJSON 函數(shù),讓其輸出到 ResponseWriter 的內(nèi)容保存到 gin 的 Context 中 (gin 框架中,每一個(gè) http 回話都與一個(gè) Context 對(duì)象綁定),這樣就可以在隨后的 middleware 能夠輕松訪問(wèn)到 response body 中的內(nèi)容。
上手開發(fā)
還是回到上一章中的 4 個(gè)核心問(wèn)題,我想到這里應(yīng)該有答案了:
- 構(gòu)建一個(gè)自定義的 ResponseWriter 實(shí)現(xiàn),覆蓋原有的 net/http 框架中 ResponseWriter,并實(shí)現(xiàn)對(duì)數(shù)據(jù)存儲(chǔ)。 -- 回答問(wèn)題 1
- 攔截 c.JSON 底層 WriteJSON 函數(shù)中的 w.Write 方法,就可以對(duì)框架無(wú)損。 -- 回答問(wèn)題 2,3
- 在 gin.Use() 函數(shù)做一個(gè)開關(guān),當(dāng) GIN_MODE 是 release 模式,就不注入這個(gè) middleware,這樣第 1,2 就不會(huì)存在,而是原有的 net/http 框架中 ResponseWriter -- 回答問(wèn)題 3,4
說(shuō)到了這么多內(nèi)容,我們來(lái)點(diǎn)實(shí)際的。
第 1 點(diǎn)代碼怎么寫
type responseBodyWriter struct {
gin.ResponseWriter // 繼承原有 gin.ResponseWriter
bodyBuf *bytes.Buffer // Body 內(nèi)容臨時(shí)存儲(chǔ)位置,這里指針,原因這個(gè)存儲(chǔ)對(duì)象要復(fù)用
}
// 覆蓋原有 gin.ResponseWriter 中的 Write 方法
func (w *responseBodyWriter) Write(b []byte) (int, error) {
if count, err := w.bodyBuf.Write(b); err != nil { // 寫入數(shù)據(jù)時(shí),也寫入一份數(shù)據(jù)到緩存中
return count, err
}
return w.ResponseWriter.Write(b) // 原始框架數(shù)據(jù)寫入
}
第 2 點(diǎn)代碼怎么寫
創(chuàng)建一個(gè) 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()
}}
創(chuàng)建一個(gè) gin middleware,用于從 pool 獲得 bytes.Buffer 指針,并創(chuàng)建 responseBodyWriter 對(duì)象覆蓋原有 gin 框架中 Context 中的 ResponseWriter,隨后清理對(duì)象回收 bytes.Buffer 指針到 pool 中。
func ginResponseBodyBuffer() gin.HandlerFunc {
return func(c *gin.Context) {
var b *bodyBuff
// 創(chuàng)建緩存對(duì)象
b = responseBodyBufferPool.Get().(*bodyBuff)
b.bodyBuf.Reset()
c.Set(responseBodyBufferKey, b)
// 覆蓋原有 writer
wr := responseBodyWriter{
ResponseWriter: c.Writer,
bodyBuf: b.bodyBuf,
}
c.Writer = &wr
// 下一個(gè)
c.Next()
// 歸還緩存對(duì)象
wr.bodyBuf = nil
if o, ok := c.Get(responseBodyBufferKey); ok {
b = o.(*bodyBuff)
b.bodyBuf.Reset()
responseBodyBufferPool.Put(o) // 歸還對(duì)象
c.Set(responseBodyBufferKey, nil) // 釋放指向 bodyBuff 對(duì)象
}
}
}
第 3 點(diǎn)代碼怎么寫
這里最簡(jiǎn)單了,寫一個(gè) if 判斷就行了。
func NewEngine(...) *Engine {
...
engine := new(Engine)
...
if gin.IsDebugging() {
engine.ginSvr.Use(ginResponseBodyBuffer())
}
...
}
看到這里,有的小伙伴就會(huì)問(wèn)了, 你還是沒(méi)有說(shuō)怎么輸出啊,我抄不到作業(yè)呢。也是哦,都說(shuō)到這里了,感覺(jué)現(xiàn)在不給作業(yè)抄,怕是有小伙伴要掀桌子。
這次“作業(yè)”的整體思路是:ginResponseBodyBuffer 在 Context 中 創(chuàng)建 bodyBuf,然后由其他的 middleware 函數(shù)處理,最終在處理函數(shù)中生成 http response,通過(guò)攔截 c.JSON 底層 WriteJSON 函數(shù)中的 w.Write 方法,記錄http response body 到之前 ginResponseBodyBuffer 生成的 bodyBuf 中。最后數(shù)據(jù)到 ginLogger 中輸出生成日志,將 http response body 輸出保存相,之后由 ginResponseBodyBuffer 回收資源。

作業(yè) 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) {
// 正常處理系統(tǒng)日志
path := GenerateRequestPath(c)
requestBody := GenerateRequestBody(c)
// 下一個(gè)
c.Next()
// response 返回
responseBody := GenerateResponseBody(c)
// 日志輸出
log.Println("path: ", path, "requestBody: ", requestBody, "responseBody", responseBody)
}
}
作業(yè) 2:日志輸出 middleware 安裝
func NewEngine(...) *Engine {
...
engine := new(Engine)
...
if gin.IsDebugging() {
engine.ginSvr.Use(ginResponseBodyBuffer(), ginLogger())
}
...
}
這里只要把 ginLogger 放在 ginResponseBodyBuffer 這個(gè) middleware 后面就可以了。
測(cè)試代碼

Console 內(nèi)容輸出
$ 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"]
服務(wù)日志輸出
{"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\"]"}
總結(jié)
我們通過(guò)上面代碼的講解和編寫,基本了解了 gin 的 webservice 框架中 response body 讀取的正確方法,以及如何在現(xiàn)有工程中集成現(xiàn)有的功能。 當(dāng)然上面所有的內(nèi)容,僅僅提供了一種解題的可能性,小伙伴應(yīng)該理解思路,結(jié)合自己的應(yīng)用場(chǎng)景,完善和改進(jìn)代碼。
以上就是go gin 正確讀取http response body內(nèi)容并多次使用詳解的詳細(xì)內(nèi)容,更多關(guān)于go gin讀取http response body的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- B站新一代 golang規(guī)則引擎gengine基礎(chǔ)語(yǔ)法
- gin通過(guò)go build -tags實(shí)現(xiàn)json包切換及庫(kù)分析
- Go語(yǔ)言開發(fā)中有了net/http為什么還要有g(shù)in的原理及使用場(chǎng)景解析
- Go gin權(quán)限驗(yàn)證實(shí)現(xiàn)過(guò)程詳解
- Go Excelize API源碼閱讀GetPageLayout及SetPageMargins
- Go?Ginrest實(shí)現(xiàn)一個(gè)RESTful接口
- Go?Gin框架路由相關(guān)bug分析
相關(guān)文章
詳解在Go語(yǔ)言單元測(cè)試中如何解決Redis存儲(chǔ)依賴問(wèn)題
在編寫單元測(cè)試時(shí),除了?MySQL?這個(gè)外部存儲(chǔ)依賴,Redis?應(yīng)該是另一個(gè)最為常見(jiàn)的外部存儲(chǔ)依賴了,本文就來(lái)講解下如何解決?Redis?外部依賴,文章通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
golang?gin框架實(shí)現(xiàn)大文件的流式上傳功能
這篇文章主要介紹了golang?gin框架中實(shí)現(xiàn)大文件的流式上傳,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07
golang?gorm實(shí)現(xiàn)get請(qǐng)求查詢案例測(cè)試
這篇文章主要為大家介紹了golang?gorm實(shí)現(xiàn)get請(qǐng)求查詢案例測(cè)試,2022-04-04
提升Golang應(yīng)用性能:深入理解Context的應(yīng)用
本文將深入探討如何通過(guò)深入理解和正確應(yīng)用Go語(yǔ)言中的Context來(lái)提升應(yīng)用性能。需要的朋友可以參考下2023-09-09
構(gòu)建Golang應(yīng)用最小Docker鏡像的實(shí)現(xiàn)
這篇文章主要介紹了構(gòu)建Golang應(yīng)用最小Docker鏡像的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

