应优先用 context.WithTimeout 精准控制请求超时,Client.Timeout 仅设总时限且不覆盖 body.Close() 阻塞;重试须基于错误类型判断,避免复用 request;本地 httptest.Server 是验证超时与重试逻辑最可靠方式。

用 http.Client 的 Timeout 和 Transport 控制请求超时
Go 的 http.Client 本身不支持“单个请求级超时”,必须靠 context.WithTimeout 或设置 Client.Timeout。但要注意:Client.Timeout 是整个请求生命周期(DNS + 连接 + TLS + 写请求 + 读响应)的总上限,无法单独控制连接或读取阶段。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 测试超时场景时,优先用
context.WithTimeout包裹http.NewRequestWithContext,这样能精准中断阻塞点(比如卡在读 body 时) - 若要模拟连接超时,可配一个极短的
http.Transport:tr := &http.Transport{DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) { dialer := &net.Dialer{Timeout: 10 * time.Millisecond} return dialer.DialContext(ctx, netw, addr) }} - 避免只设
Client.Timeout就以为覆盖所有情况——它不作用于RoundTrip返回后的 body.Close() 阻塞,这点常被忽略
用 retryablehttp 或手动实现重试逻辑时如何注入失败信号
标准 net/http 不带重试,得靠第三方库(如 hashicorp/go-retryablehttp)或手写。关键在于:重试决策必须基于错误类型,而不是状态码。比如 context.DeadlineExceeded 或 net.OpError 才该重试,401 或 404 通常不该重试。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
errors.Is(err, context.DeadlineExceeded)判断是否超时错误,不要用strings.Contains(err.Error(), "timeout") - 手写重试循环时,每次迭代必须新建
*http.Request,不能复用旧 request —— 它的 body 可能已被读过或关闭 - 若用
retryablehttp.Client,注意它的默认策略会重试5xx和网络错误,但不会重试4xx;可通过RetryMax、RetryWaitMin等字段调整,但无法细粒度区分“连接失败”和“响应超时”
用本地 HTTP 服务模拟超时与随机失败(无需外部依赖)
测试重试和超时,最可靠的方式是起一个可控的本地服务,而非 mock http.Client。Go 自带 httptest.Server 支持延迟响应、随机 panic、提前断连等行为。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 模拟超时:在 handler 中调用
time.Sleep(2 * time.Second),再配合客户端context.WithTimeout(ctx, 1*time.Second) - 模拟连接拒绝:handler 里直接
panic("simulated dial failure"),然后用httptest.NewUnstartedServer+srv.Start()控制启动时机 - 模拟读取中断:用
conn, _ := srv.Listener.Addr().(*net.TCPAddr)手动建立net.Conn,写 header 后不写 body,让 client 卡在resp.Body.Read
验证重试次数与间隔是否符合预期
重试逻辑是否真正生效,不能只看最终成功与否,得确认中间请求是否发出、间隔是否准确、是否在正确错误下触发。最直接的方式是记录每次请求的发起时间与错误。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 在重试循环内打日志:
log.Printf("attempt %d at %v, err: %v", i, time.Now(), err),比断言更直观 - 用
httptest.Server的Config.Handler包一层计数器:var callCount int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { callCount++ // ... your logic }))测试后断言callCount == 3 - 注意:某些重试库(如
retryablehttp)的指数退避基于 wall clock,不是从首次失败开始算,而是每次重试前 sleep,所以实际间隔可能略大于理论值
超时与重试的组合逻辑容易在边界处失效——比如超时后重试,但新请求又因同一原因失败,而重试库没做错误归因;或者 context 被 cancel 后,transport 仍试图复用底层连接。这些都需要在本地可控服务中反复触发才能暴露。










