在写一个Gin框架日志中间件的时候,需要记录请求和响应相关的一些数据,例如请求参数、请求方法、请求时间、请求头、耗时、响应状态码、响应数据等,gin为获取这些数据基本都提供了现成的方法,但是获取响应数据还是有一定难度和复杂度的。
那么,该如何获取响应数据也就是 response body 呢?先上代码。
代码示例
1、先写一个 middleware ,简单打印一下 response body
package middleware
import (
"bytes"
"fmt"
"github.com/gin-gonic/gin"
)
//自定义一个结构体,实现 gin.ResponseWriter interface
type responseWriter struct {
gin.ResponseWriter
b *bytes.Buffer
}
//重写 Write([]byte) (int, error) 方法
func (w responseWriter) Write(b []byte) (int, error) {
//向一个bytes.buffer中写一份数据来为获取body使用
w.b.Write(b)
//完成gin.Context.Writer.Write()原有功能
return w.ResponseWriter.Write(b)
}
func PrintResponse(c *gin.Context) {
writer := responseWriter{
c.Writer,
bytes.NewBuffer([]byte{}),
}
c.Writer = writer
c.Next()
fmt.Println("response body:" + writer.b.String())
}
2、引用 middleware ,看看效果
package main
import (
"github.com/gin-gonic/gin"
"hello/middleware"
"net/http"
)
func main() {
r := gin.New()
//添加获取响应内容 middleware
r.Use(middleware.PrintResponse)
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"name": "xiaoming"})
})
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}
打开浏览器访问 http://127.0.0.1:8080/test ,即可在控制台中看到 response body 的输出内容
response body:{"name":"xiaoming"}
原理
通过上面的代码可以看出,gin 通过调用如下方法写入了 response body
c.JSON(http.StatusOK, gin.H{"name": "xiaoming"})
追进 JSON 方法,源码如下,调用了 Render 方法
package gin
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
追进 Render 方法, 可以看出 gin.Context.Writer 作为参数传给了 r.Render() 方法,这里形参 r 的实参为 render.JSON{Data: obj} ,所以实际调用的是 func (r JSON) Render(w http.ResponseWriter) 方法。
package gin
// 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)
}
}
func (r JSON) Render(w http.ResponseWriter) 源码如下
package render
// 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 interface{}) error {
writeContentType(w, jsonContentType)
jsonBytes, err := json.Marshal(obj)
if err != nil {
return err
}
_, err = w.Write(jsonBytes)
return err
}
在 func (r JSON) Render() 方法中,gin.Context.Writer 被传到了 WriteJSON() 方法中,最终写入数据调用的是 gin.Context.Writer.Write() 方法。gin.ResponseWriter 源码如下
package gin
type Context struct {
//...
writermem responseWriter
Writer ResponseWriter
//...
}
// ResponseWriter ...
type ResponseWriter interface {
//...
http.ResponseWriter
//...
}
可以看出 gin.Context.Writer 类型为 interface gin.ResponseWriter。
package http
type ResponseWriter interface {
//...
Write([]byte) (int, error)
//...
}
要实现 gin.ResponseWriter 接口,必须实现Write([]byte) (int, error) 方法。所以写入 response body 调用的是 gin.Context.Writer.Write() 方法,gin.Context.Writer 需要是type gin.ResponseWriter interface 的一个具体实现。
到此,可以看出上面代码示例的思路:
实现 type gin.ResponseWriter interface 并重写 Write([]byte) (int, error) 方法,该方法在实现 gin.Context.Writer.Write() 原有功能的同时,再向一个 bytes.buffer 中写一份数据来用于获取 response body 使用。