
本文介绍如何在 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 <p>✅ <strong>关键设计要点说明</strong>:</p>
-
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 中间件的基石实践。










