搬运代码, 高高兴兴
gin 是 Golang 中很火的 Web 框架. 最近我有一个拦截 gin response 返回值并记录日志的需求.
显然使用 gin 的 middleware 来实现最合适. 我搜索后发现 github 上 gin issue 中有人给出了相关实现,还有好几个赞, 于是乎我就高高兴兴的把代码抄下来了.
当时我也测了一下, 发现没毛病, 便稍加改动上线了 …
不好, 有 bug 了!
结果上线不到一天, 我就发现问题了, 咦, 怎么有的 response 返回值没有输出呢?
经过一番探索, 这个 bug 在我本地复现了. 上面代码中函数 sayHello 调用 c.JSON 来响应, 如果改为调用 c.String 也就是response 返回值为一个字符串, 那么 logResponseBody 这个 middleware 函数就 hook 不到 response 返回值了.
// sayHello 这样改动后,
// logResponseBody 这个 middleware 函数
// 就 hook 不到 response 返回值了
func sayHello(c *gin.Context) {
//c.JSON(200, gin.H{
//"hello": "privat io nel",
//})
c.String(200, "hello world")
}
解析 github 上的代码
我搬运的代码:
代码中 responseBodyWriter 结构体 实现的是 gin.ResponseWriter 接口.
注意一下其中的 Write 方法, 这个方法把 response 返回值缓存到 responseBodyWriter 结构体的 body 属性中, 后面会用来输出 response 返回值.
我对相关代码, 加了注释:
func (r responseBodyWriter) Write(b [] byte ) (int, error) {
// b 就是 response
// Write 方法把 response 缓存到 responseBodyWriter 结构体的 body 属性中
r.body.Write(b)
return r.ResponseWriter.Write(b)
}
最终 logResponseBody 函数负责打印 response 返回值, 结合相关注释理解下:
func logResponseBody(c *gin.Context) {
w := &responseBodyWriter{body: & bytes .Buffer{}, ResponseWriter: c.Writer}
// 使用 responseBodyWriter 替换 gin 中的 responseWriter,
// 替换的目的是把 response 返回值缓存起来
c.Writer = w
c.Next()
// 打印 response 的返回值
fmt.Println("Response body: " + w.body.String())
}
搞懂 gin response 返回值的原理, 揪出 bug
如果你看懂了上文, 那么这个 bug 也就不难找了.
c.JSON , c.String 等 response 方法都实现的 Render 包中的 Render 接口. 不同的是, 在实现 Render 接口的 Render 方法, c.JSON 调用了 gin.ResponseWriter.Write 方法输出返回值; 而 c.String 调用的是 gin.ResponseWriter.WriteString 输出返回值. 函数调用 关系图如下:
我把相关的源码贴上, 再来验证一下.
c.JSON 的 Render 方法中调用了 WriteJSON, 在 WriteJSON 中调用的是 ResponseWriter.Write 方法. 源码如下图所示.
c.String 的 Render 方法中调用了 WriteString, 在 WriteString 中调用的是 io.WriteString.
io.WriteString 的实现, 如下图所示, 调用的是 sw.WriteString, 也就是 gin.ResponseWriter 接口中的 WriteString 方法, 而高票答案中没有实现这个方法.
修复bug
理解了以上内容, 修复 bug 也非常简单, 只需要重写一下 WriteString 方法就可以了:
func (r responseBodyWriter) WriteString(s string) (n int, err error) {
r.body.WriteString(s)
return r.ResponseWriter.WriteString(s)
}
补充
查看扩展链接, 可以看到 github 上的那个 issue.