应优先用 context.WithTimeout 精准控制请求超时,Client.Timeout 仅设总时限且不覆盖 body.Close() 阻塞;重试须基于错误类型判断,避免复用 request;本地 httptest.Server 是验证超时与重试逻辑最可靠方式。
用 http.Client 的 Timeout 和 Transport 控制请求超时
Go 的 http.Client 本身不支持“单个请求级超时”,必须靠 context.WithTimeout 或设置 Client.Timeout。但要注意:Client.Timeout 是整个请求生命周期(DNS + 连接 + TLS + 写请求 + 读响应)的总上限,无法单独控制连接或读取阶段。
实操建议:
- 测试超时场景时,优先用
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 通常不该重试。
实操建议:
- 用
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、提前断连等行为。