我的编程空间,编程开发者的网络收藏夹
学习永远不晚

go gin如何正确读取http response body内容并多次使用

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

go gin如何正确读取http response body内容并多次使用

这篇文章主要介绍了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.ResponseWriterbodyBuf *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)// 覆盖原有 writerwr := responseBodyWriter{ResponseWriter: c.Writer,bodyBuf:        b.bodyBuf,}c.Writer = &wr// 下一个c.Next()// 归还缓存对象wr.bodyBuf = nilif 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 OKAccess-Control-Allow-Credentials: trueAccess-Control-Allow-Headers: Content-Type, AccessToken, X-CSRF-Token, Authorization, TokenAccess-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-TypeContent-Type: application/json; charset=utf-8X-Request-Id: 1611289702609555456Date: Fri, 06 Jan 2023 09:12:56 GMTContent-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内容并多次使用”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网行业资讯频道。

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    go gin如何正确读取http response body内容并多次使用

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档

    猜你喜欢

    go gin如何正确读取http response body内容并多次使用

    这篇文章主要介绍了go gin如何正确读取http response body内容并多次使用的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇go gin如何正确读取http response body内容并多次使
    2023-07-04

    go gin 正确读取http response body内容并多次使用详解

    这篇文章主要为大家介绍了go gin 正确读取http response body内容并多次使用解决思路,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-08

    编程热搜

    • Python 学习之路 - Python
      一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
      Python 学习之路 - Python
    • chatgpt的中文全称是什么
      chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
      chatgpt的中文全称是什么
    • C/C++中extern函数使用详解
    • C/C++可变参数的使用
      可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
      C/C++可变参数的使用
    • css样式文件该放在哪里
    • php中数组下标必须是连续的吗
    • Python 3 教程
      Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
      Python 3 教程
    • Python pip包管理
      一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
      Python pip包管理
    • ubuntu如何重新编译内核
    • 改善Java代码之慎用java动态编译

    目录