
本文介绍如何在 Go HTTP 服务中安全实现带超时控制的中间件,重点解决 ResponseWriter 并发写入导致的竞态问题:通过 responseRecorder 拦截并延迟写入响应,确保超时与业务处理互斥、线程安全。
本文介绍如何在 go http 服务中安全实现带超时控制的中间件,重点解决 `responsewriter` 并发写入导致的竞态问题:通过 `responserecorder` 拦截并延迟写入响应,确保超时与业务处理互斥、线程安全。
在 Go 的 HTTP 服务中,为 handler 添加超时逻辑是常见需求,但若直接在新 goroutine 中调用下游 handler 并并发操作 http.ResponseWriter(如 fmt.Fprint(w, ...) 或 w.WriteHeader()),将引发严重的竞态条件(race condition)——因为 http.ResponseWriter 不是并发安全的,多个 goroutine 同时调用其方法可能导致 panic、响应内容错乱或状态码覆盖。
核心问题有两个:
- ✅ 问题1:当上下文超时时,必须阻止下游 handler 继续向原始
w写入; - ✅ 问题2:当下游 handler 正在写入但尚未完成时,超时 goroutine 不应抢先调用
http.Error(w, ...),否则会破坏响应完整性(如重复写 header、状态码冲突)。
标准解法是隔离响应写入行为:不将原始 ResponseWriter 直接传给下游 handler,而是传入一个线程安全的、可回放的 responseRecorder,由它捕获所有响应数据(状态码、Header、Body),待超时判定结束后,再原子性地将结果复制到真实 ResponseWriter 上。
以下是完整、可运行的解决方案:
Miller (mlr) 是一个命令行工具,用于查询、整形和重新格式化名称索引数据,如 CSV、TSV、JSON 和 JSON Lines。它将 awk、sed、cut、join 和 sort 的功能整合到一个专为结构化数据处理而构建的单一工具中。
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"time"
)
func main() {
http.Handle("/race", handlerFunc(timeoutHandler))
http.ListenAndServe(":8080", nil)
}
func timeoutHandler(w http.ResponseWriter, r *http.Request) error {
const seconds = 1
ctx, cancel := context.WithTimeout(r.Context(), time.Second*seconds)
defer cancel()
r = r.WithContext(ctx)
errCh := make(chan error, 1)
w2 := newResponseRecorder() // 使用 recorder 替代原始 w
go func() {
errCh <- nextHandler(w2, r) // 下游 handler 只操作 w2
}()
select {
case err := <-errCh:
if err != nil {
return err
}
// 安全:仅在 select 胜出后,单线程写入真实响应
w2.cloneHeader(w.Header())
w.WriteHeader(w2.status)
w.Write(w2.buf.Bytes())
return nil
case <-ctx.Done():
// 超时:此时 nextHandler 可能仍在运行,但绝不会写入 w
http.Error(w, "Request timeout", http.StatusRequestTimeout)
return nil
}
}
func nextHandler(w http.ResponseWriter, r *http.Request) error {
// 模拟耗时处理(例如 DB 查询、外部 API 调用)
time.Sleep(1500 * time.Millisecond) // 故意超过 1s 超时阈值
fmt.Fprint(w, "nextHandler processed successfully")
return nil
}
// responseRecorder 实现了 http.ResponseWriter 接口,用于捕获响应
type responseRecorder struct {
http.ResponseWriter
header http.Header
buf *bytes.Buffer
status int
}
func newResponseRecorder() *responseRecorder {
return &responseRecorder{
header: make(http.Header),
buf: &bytes.Buffer{},
status: 0,
}
}
func (w *responseRecorder) Header() http.Header {
return w.header
}
func (w *responseRecorder) WriteHeader(status int) {
w.status = status
}
func (w *responseRecorder) Write(data []byte) (int, error) {
if w.status == 0 {
w.status = http.StatusOK
}
return w.buf.Write(data)
}
// cloneHeader 深拷贝 header 到目标 Header,避免引用共享
func (w *responseRecorder) cloneHeader(dst http.Header) {
for k, v := range w.header {
dst[k] = append(dst[k][:0], v...)
}
}
type handlerFunc func(http.ResponseWriter, *http.Request) error
func (fn handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
http.Error(w, "Server error", http.StatusInternalServerError)
}
}✅ 关键设计要点说明:
-
responseRecorder是轻量级内存缓冲器,不涉及 I/O,无并发风险; -
nextHandler始终只操作w2,即使它在超时后仍在执行,也不会干扰主 goroutine 对真实w的操作; -
select保证了「成功响应」和「超时响应」二者有且仅有一个分支被执行,天然互斥; -
cloneHeader使用append(dst[k][:0], v...)实现 header 值的安全复制(避免 slice 底层共用数组); - 状态码默认兜底为
200 OK(当 handler 未显式调用WriteHeader时),符合http.ResponseWriter行为规范。
⚠️ 注意事项:
- 此方案适用于常规 HTTP handler 场景;若 handler 内部使用
http.Flusher、http.Hijacker等高级接口,则需扩展responseRecorder支持对应方法; -
responseRecorder.buf无大小限制,对超大响应需增加长度检查,防止 OOM; - 在生产环境建议结合
context取消信号通知下游 handler 主动中断(如数据库查询取消),而非仅依赖超时“静默丢弃”。
该模式已被 net/http 官方中间件生态广泛采用(如 gorilla/handlers.TimeoutHandler 的简化思想),是构建健壮、可观测、线程安全 HTTP 中间件的基石实践。

















