c.Writer 不是 http.ResponseWriter 本身,而是 Gin 包了一层、记录状态和字节数的 responseWriter。这篇看这一层怎么工作:Status、Size、Written 在一次请求里如何变化,状态为什么写出后改不了,自己再包一层抓响应体时容易漏掉什么,以及 Hijack 的限制。
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 模式下完全静默——线上排查“为什么状态码不对”时要想到这一点。
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: 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,因为这时连接已经不干净了。