bigywater.tech
Gin · 28

ResponseWriter 的包装:Status、Size、Written

c.Writer 不是 http.ResponseWriter 本身,而是 Gin 包了一层、记录状态和字节数的 responseWriter。这篇看这一层怎么工作:Status、Size、Written 在一次请求里如何变化,状态为什么写出后改不了,自己再包一层抓响应体时容易漏掉什么,以及 Hijack 的限制。

-1 → 0 → 11
Size() 的变化:未写出 -1,WriteHeaderNow 后 0,写完 hello world 后 11
3 / 5
只覆盖 Write 的包装器能抓到的写法个数(漏掉 WriteString 与 io.WriteString)
already written
Hijack 在正文写出之后返回的错误:gin: response body already written

size 与 status

实测 · 本机执行输出
  response_writer.go 的 responseWriter(Gin v1.12.0):
      42 |     // WriteHeaderNow forces to write the http header (status code + headers).
      43 |     WriteHeaderNow()
      44 | 
      45 |     // Pusher get the http.Pusher for server push
      46 |     Pusher() http.Pusher
      47 | }
      48 | 
      49 | type responseWriter struct {
      50 |     http.ResponseWriter
      51 |     size   int
      52 |     status int
      53 | }
      54 | 
      55 | var _ ResponseWriter = (*responseWriter)(nil)
      56 | 
  response_writer.go 的 reset / WriteHeader / Write(Gin v1.12.0):
      58 |     return w.ResponseWriter
      59 | }
      60 | 
      61 | func (w *responseWriter) reset(writer http.ResponseWriter) {
      62 |     w.ResponseWriter = writer
      63 |     w.size = noWritten
      64 |     w.status = defaultStatus
      65 | }
      66 | 
      67 | func (w *responseWriter) WriteHeader(code int) {
      68 |     if code > 0 && w.status != code {
      69 |         if w.Written() {
      70 |             debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code)
      71 |             return
      72 |         }
      73 |         w.status = code
      74 |     }
      75 | }
      76 | 
      77 | func (w *responseWriter) WriteHeaderNow() {
      78 |     if !w.Written() {
      79 |         w.size = 0
      80 |         w.ResponseWriter.WriteHeader(w.status)
      81 |     }
      82 | }
      83 | 
      84 | func (w *responseWriter) Write(data []byte) (n int, err error) {
      85 |     w.WriteHeaderNow()
      86 |     n, err = w.ResponseWriter.Write(data)
      87 |     w.size += n
      88 |     return
      89 | }
      90 | 

Gin 用 size = -1 表示“还没写任何东西”,WriteHeaderNow 之后变成 0,每次写入再累加。c.Status 只是记录状态;状态行要等到第一次写正文或手工 WriteHeaderNow 才真正发出,之后再改就被忽略。

一次请求里的变化

实测 · 本机执行输出
    handler 刚进入                        Status()=200 Size()=-1  Written()=false
    c.Header("X-A", "1")               Status()=200 Size()=-1  Written()=false
    c.Status(201)                      Status()=201 Size()=-1  Written()=false
    再 c.Status(202)(还没写,可以改)           Status()=202 Size()=-1  Written()=false
    c.Writer.WriteHeaderNow()          Status()=202 Size()=0   Written()=true
    再 c.Status(500)(已经发出,改不动)          Status()=202 Size()=0   Written()=true
    WriteString("hello")               Status()=202 Size()=5   Written()=true
    Write(" world")                    Status()=202 Size()=11  Written()=true
    最终客户端:状态=202 正文="hello world"

handler 刚进入时 Status() 是默认的 200、Size() 是 -1、Written() 为 false;c.Header 不影响它们;c.Status 可以反复改;WriteHeaderNow 之后 Size() 变成 0、Written() 为 true、状态锁定,再写正文时 Size() 累加。注意 Written() 表示“状态行已发出”,不等于“写过正文”。

写出之后再改状态

实测 · 本机执行输出
  [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
   - using env:	export GIN_MODE=release
   - using code:	gin.SetMode(gin.ReleaseMode)
  
  [GIN-debug] GET    /x                        --> main.main.func1 (1 handlers)
  [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 200 with 500

状态行一旦发出就锁定。debug 模式下再改会打印“Headers were already written”的警告,release 模式下完全静默——线上排查“为什么状态码不对”时要想到这一点。

包装 ResponseWriter

实测 · 本机执行输出
    c.String                         客户端收到 "hello str"  | 只覆盖 Write:抓到 "hello str"  | 再覆盖 WriteString:抓到 "hello str"
    c.JSON                           客户端收到 "{\"a\":1}"  | 只覆盖 Write:抓到 "{\"a\":1}"  | 再覆盖 WriteString:抓到 "{\"a\":1}"
    c.Data                           客户端收到 "data!"      | 只覆盖 Write:抓到 "data!"      | 再覆盖 WriteString:抓到 "data!"
    c.Writer.WriteString             客户端收到 "direct"     | 只覆盖 Write:抓到 ""           | 再覆盖 WriteString:抓到 "direct"
    io.WriteString(c.Writer, ...)    客户端收到 "iows"       | 只覆盖 Write:抓到 ""           | 再覆盖 WriteString:抓到 "iows"

日志、压缩、签名类中间件常常包装 c.Writer 来拦截响应体。gin.ResponseWriter 接口里 WriteString 是独立于 Write 的方法,所以只覆盖 Write 的包装器会漏掉直接调用 c.Writer.WriteString 或 io.WriteString(c.Writer, …) 的代码。c.String、c.JSON、c.Data 走的是 Write,所以不受影响。

Hijack 的限制

实测 · 本机执行输出
    写正文前 Hijack: conn!=nil true err=<nil>
      客户端收到 200 "hi"
    写正文后 Hijack: err=gin: response body already written
      客户端收到 200 "already"

正文还没写出时可以 Hijack 拿到底层连接(WebSocket 的升级就是这么做的)。一旦写过正文(size > 0),Hijack 返回 gin: response body already written,因为这时连接已经不干净了。

分步演示

ResponseWriter
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。前三节用 httptest 直接调用 ServeHTTP(第三节用 GIN_MODE=debug);第四节同时覆盖了 5 种写法;第五节用真实的 httptest.NewServer 与 http.Client;每种情形各一次
    • 没有验证:Flush / Pusher / CloseNotify 经过包装器时的行为;压缩中间件(gzip)对 Size() 的影响;http.Hijacker 在 HTTP/2 下的情形(HTTP/2 不支持 Hijack,没有跑);其他版本的 responseWriter 实现
    京ICP备2026067502号-1京公网安备11010502063487号