Gin 优雅打印请求与回包内容

邰昀
2023-12-01


在开发 Web 应用程序时,难免不会遇到功能或性能等问题。为了快速定位问题,需要打印请求和响应的内容。

本文将介绍如何使用 Gin 框架来优雅地打印请求和响应的内容。

1.Gin 的 Middleware

Gin 是一种轻量级的 Web 框架,用于构建高性能的 Web 应用程序。它具有快速、简单和易于使用的特点,并且具有许多可扩展的功能,如中间件。

在 Gin 框架中,中间件是一种用于拦截 HTTP 请求和响应的机制。中间件函数可以在请求到达处理函数之前或之后执行某些操作,例如:

  • 登录态校验
  • 权限校验
  • 打印请求和响应的内容
  • 设置接口超时等

Gin 框架提供了一种简单的方法来定义和使用中间件。中间件函数需要满足以下条件:

  • 函数的签名必须是 func(c *gin.Context),其中 c 是 Gin 框架中的上下文对象。
  • 函数可以执行任何操作,但是必须调用 c.Next() 方法来继续执行请求处理程序和其他中间件函数。
  • 如果需要在请求处理程序之后执行某些操作,可以在调用 c.Next() 之后执行。

2.使用 Middleware 打印请求与回包内容

下面是一个使用 Gin 中间件来打印请求和响应内容的示例代码:

func LoggerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
    	// 记录请求时间
		start := time.Now()

        // 打印请求信息
        reqBody, _ := c.GetRawData()
        fmt.Printf("[INFO] Request: %s %s %s\n", c.Request.Method, c.Request.RequestURI, reqBody)
        
        // 执行请求处理程序和其他中间件函数
        c.Next()

        // 记录回包内容和处理时间
		end := time.Now()
		latency := end.Sub(start)
		respBody := string(c.Writer.Body.Bytes())
		fmt.Printf("[INFO] Response: %s %s %s (%v)\n", c.Request.Method, c.Request.RequestURI, respBody, latency)
    }
}

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

    // 注册中间件
    r.Use(Logger())

    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Hello, World!")
    })

    r.Run(":8080")
}

上面的代码定义了一个中间件,用来记录请求和回包内容。在中间件中,我们首先记录请求的时间和请求内容,然后调用 c.Next() 继续处理请求。在请求处理完成后,我们记录回包内容和处理时间。最后,我们使用 gin.Default() 函数来创建一个 Gin 引擎实例,并注册路由和中间件。启动服务后,我们可以访问http://localhost:8080/hello查看请求和回包的内容。

3.多次读取请求 Body 的问题

实际上,上面的做法会有问题。

在中间件中读取了请求的 Body,如果在接口处理函数中再次读取 Body,会导致 Body 被读取两次,从而出现问题。 因为在读取 Body 后,Body 的指针会被移到末尾,第二次读取时就无法再次读取到内容。

那么 Gin 如何正确多次读取 http request body 的内容呢?

解决思路: 由于 Request.Body 为公共变量,我们在对原有的 buffer 读取完成后,只要手动创建一个新的 buffer 然后以同样接口形式替换掉原有的 Request.Body 即可。

reqBytes, _ := c.GetRawData()

// 请求包体写回。
if len(reqBytes) > 0 {
	c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBytes))
}

在 Go 语言中,io.NopCloser 函数返回一个实现 io.ReadCloser 接口的对象,这个对象可以包装任何实现 io.Reader 接口的对象,并提供了一个空的 Close 方法。这个方法的作用就是什么也不做,仅仅是返回一个 nil 的 error。

通常情况下,我们会使用 io.ReadCloser 接口读取数据,并在读取完成后关闭相关资源,例如打开的文件句柄或者网络连接等。但是在某些场景下,我们希望读取数据,但是并不想关闭相关资源,比如在数据读取完成后还需要进行一些其他操作,或者需要多次读取同一个资源等。

这时候,就可以使用 io.NopCloser 函数将一个实现了 io.Reader 接口的对象包装成一个实现了 io.ReadCloser 接口的对象,并在 Close 方法中什么也不做。这样就可以在读取数据后不关闭相关资源,从而方便进行其他操作或者多次读取同一个资源。

4.多次读取响应 Body 的问题

同样地,在中间件中读取响应 Body 的问题是,它会使得缓冲区被读取完毕,指针指向了缓冲区的末尾,而后续的代码再次读取 Body 时,指针已经到了缓冲区的末尾,无法再次读取。

为了避免这个问题,我们可以使用一个自定义的 ResponseWriter 来替换 Gin 默认的 ResponseWriter。自定义的 ResponseWriter 可以将响应 Body 写入到一个内存缓冲区中,并在中间件中获取响应 Body 并记录日志。

下面是一个完整的可以在日志中间件中读取请求与响应 Body 的示例。

// CustomResponseWriter 封装 gin ResponseWriter 用于获取回包内容。
type CustomResponseWriter struct {
	gin.ResponseWriter
	body *bytes.Buffer
}

func (w CustomResponseWriter) Write(b []byte) (int, error) {
	w.body.Write(b)
	return w.ResponseWriter.Write(b)
}

// 日志中间件。
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
    	// 记录请求时间
		start := time.Now()

		// 使用自定义 ResponseWriter
		crw := &CustomResponseWriter{
			body:           bytes.NewBufferString(""),
			ResponseWriter: c.Writer,
		}
		c.Writer = crw

        // 打印请求信息
        reqBody, _ := c.GetRawData()
        fmt.Printf("[INFO] Request: %s %s %s\n", c.Request.Method, c.Request.RequestURI, reqBody)

        // 执行请求处理程序和其他中间件函数
        c.Next()

        // 记录回包内容和处理时间
		end := time.Now()
		latency := end.Sub(start)
		respBody := string(crw.body.Bytes())
		fmt.Printf("[INFO] Response: %s %s %s (%v)\n", c.Request.Method, c.Request.RequestURI, respBody, latency)
    }
}

5.小结

在本文中,我们介绍了为什么要打印请求与回包内容,以及如何使用 Gin 的 Middleware 功能来打印请求和回包内容。通过打印请求和回包内容,我们可以更好地了解 API 的执行过程,并且可以快速定位问题。


参考文献

OpenAI ChatGPT
Using middleware | Gin Web Framework
如何让gin正确多次读取http request body内容- 掘金
如何让gin 正确读取http response body 内容,并多次使用- 掘金

 类似资料: