应使用 context.WithTimeout 为每次请求创建独立上下文并传入 http.NewRequestWithContext,配合 defer cancel() 防泄漏;重试仅针对 5xx、429 及 net.OpError 等临时错误,采用带抖动的指数退避。

用 context.WithTimeout 控制单次请求超时
直接依赖 http.Client.Timeout 虽简单,但无法在请求中途(比如已发请求但未收响应头)精确取消。真正可控的方式是把超时逻辑下沉到请求上下文里。
每次发起请求前,必须用 context.WithTimeout 创建新上下文,并传给 http.NewRequestWithContext —— 这样即使 Transport 层卡在读响应体,也能被及时中断。
- 不要复用同一个
ctx多次调用,否则第二次调用可能收到已取消的上下文 -
defer cancel()必须紧接在WithTimeout后,避免 goroutine 泄漏 - 若需传递 traceID 或其他值,应从原始
ctx派生,而非用context.Background()
重试前先判断错误是否可重试
不是所有错误都适合重试。401、404、400 是客户端问题,重试只会放大无效流量;而 5xx、429、连接类错误(如 net.OpError、url.Error)才属于临时性故障。
推荐用类型断言 + 错误码双重判断:
- 检查
err是否为*url.Error,再看其Err字段是否是net.OpError - 拿到
resp.StatusCode后,用status >= 500 || status == 429判断服务端异常 - 对 TLS 握手失败、DNS 解析超时等底层错误,统一归为可重试
用指数退避避免打爆下游
固定间隔重试(如每次等 100ms)在高并发下容易触发雪崩。真实场景中应采用指数退避:第 n 次重试等待 base * 2^(n-1),并叠加随机抖动防止同步冲击。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
示例逻辑(不依赖第三方库):
func retryWithBackoff(max int, base time.Duration, fn func() error) error {
var err error
for i := 0; i
- 首次重试延迟设为
100ms较稳妥,太短没意义,太长影响 SLA - 最大重试次数建议 ≤ 3,超过说明问题已非临时性
- 注意:该函数不能用于非幂等请求(如 POST 创建资源),否则可能重复提交
组合 context 和重试形成完整链路
总超时必须覆盖所有重试耗时,否则最后一次重试可能因父 context 已取消而立即失败。常见错误是只给单次请求设超时,却忘了外层重试循环本身也需要时间预算。
正确做法:
- 外层用
context.WithTimeout(ctx, totalTimeout)约束整个重试过程 - 每次重试内,再用
context.WithTimeout(outerCtx, perRequestTimeout)约束单次请求 -
perRequestTimeout应明显小于totalTimeout / maxRetries,留出退避和调度开销 - 一旦外层
ctx.Err() != nil,立刻终止重试,不再进入下一轮
最易被忽略的是:重试过程中若某次请求成功返回了 *http.Response,但后续读取 resp.Body 时超时或出错,这个错误仍需纳入重试判定 —— 因为 body 读取失败往往意味着连接已断或服务异常,不是客户端逻辑问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










