httptest不支持真并发,需用goroutine+sync.WaitGroup手动压测:每个goroutine必须独立构造http.Request和httptest.ResponseRecorder,避免共享资源导致panic或数据错乱;其本质是同步调用handler的并发模拟,仅用于验证逻辑线程安全,非真实网络压测。

httptest 不支持真并发,得用 goroutine + sync.WaitGroup 手动压测
httptest 的 ResponseRecorder 是单次请求上下文,调用 router.ServeHTTP(w, req) 是同步阻塞的,没有内置并发能力。所谓“模拟并发”,本质是手动启多个 goroutine 并发调用 handler,靠 sync.WaitGroup 控制生命周期,而不是依赖测试框架自动调度。
常见错误现象:
- 直接在 for 循环里启动 goroutine 但没 wait → 测试函数提前结束,goroutine 被强制终止
- 共享同一个
*http.Request实例 → 多个 goroutine 同时读 body,触发http: read on closed response body - 复用
*httptest.ResponseRecorder→ 多个 handler 写同一块内存,结果错乱或 panic
实操建议:
- 每个 goroutine 必须独立构造
*http.Request(哪怕 URL 和 method 相同) - 每个 goroutine 分配独立的
*httptest.ResponseRecorder - 用
sync.WaitGroup等待全部完成,再统一断言 - 避免在 handler 内部做全局状态修改(如写文件、改全局 map),否则并发下结果不可预测
func TestConcurrentPingHandler(t *testing.T) {
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})
<pre class="brush:php;toolbar:false;">var wg sync.WaitGroup
results := make(chan int, 100) // 收集状态码
for i := 0; i <p>}</p>
要测并发安全,重点不是请求数,而是共享资源访问路径
并发测试真正的价值不在“能不能扛住 100 QPS”,而在于暴露 handler 内部对非线程安全对象的误用。比如:
- 直接往全局
map[string]string写数据,没加锁 →fatal error: concurrent map writes - 复用未初始化的 struct 指针(如
var u User在 handler 外定义)→ 字段值被多个请求覆盖 - 中间件里把 context.Value 存到全局变量 → 并发下 key/value 错位
所以并发测试前,先人工检查 handler 是否有以下行为:
- 是否读写任何包级变量(
var声明在函数外) - 是否调用未加锁的
sync.Map以外的 map 操作 - 是否依赖
time.Now()或rand.Intn()等非确定性函数且未隔离 seed
别拿测试环境当生产压测工具
基于 httptest 的并发测试只是逻辑层并发验证,它绕过了 TCP 层、TLS 握手、连接池、反向代理等真实链路环节。它的瓶颈永远是本机 CPU 和 Go runtime 调度,而非网络 IO。
这意味着:
- 测出 5000 QPS ≠ 生产能撑住 5000 QPS;实际可能卡在 nginx 连接数或数据库连接池
- 测不出慢 SQL、锁等待、GC STW 等真实延迟毛刺
- 无法验证超时控制是否生效(因为
http.Request在测试中不走真实网络超时)
如果真需要评估吞吐,应该用 ab、hey 或 vegeta 对已部署的服务发起真实 HTTP 请求,而不是在单元测试里堆 goroutine。
真正该在单元测试里关注的,并发下 handler 是否仍保持语义正确 —— 比如两次并发登录,是否生成了两个独立 session,而不是互相覆盖。这点容易被忽略,但恰恰是并发 bug 最常藏身之处。











