
1. 搬运代码, 高高兴兴
gin 是 Golang 中很火的 Web 框架. 最近我有一个拦截 gin response 返回值并记录日志的需求.
显然使用 gin 的 middleware 来实现最合适. 我搜索后发现 github 上 gin issue 中有人给出了相关实现,还有好几个赞, 于是乎我就高高兴兴的把代码抄下来了.
当时我也测了一下, 发现没毛病, 便稍加改动上线了 ...
2. 不好, 有 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": "privationel",
//})
c.String(200, "hello world")
}
3. 解析 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())
}
4 揪出 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
方法, 而高票答案中没有实现这个方法.

5. 修复bug
理解了以上内容, 修复 bug 也非常简单, 只需要重写一下 WriteString
方法就可以了:
func (r responseBodyWriter) WriteString(s string) (n int, err error) {
r.body.WriteString(s)
return r.ResponseWriter.WriteString(s)
}