推荐使用 github.com/avast/retry-go 实现带指数退避的重试,它封装了退避策略、次数控制、错误过滤和上下文取消,避免手写循环导致的 goroutine 泄露、固定延迟压垮下游等问题。

Go 里用 retry.Do 实现带指数退避的重试,别自己手写循环
标准库不提供重试,但社区公认最稳的是 github.com/avast/retry-go。它把退避、最大尝试次数、错误过滤、上下文取消都封装好了,比手撸 for + time.Sleep 可靠得多——尤其在并发场景下容易漏掉 ctx.Done() 检查,导致 goroutine 泄露。
常见错误现象:context deadline exceeded 后还在继续重试;或重试间隔固定为 100ms,压垮下游服务。
- 用
retry.Attempts(3)控制总次数,不是“最多重试 3 次”,而是“总共执行 3 次”(含首次) -
retry.Delay(100 * time.Millisecond)是初始延迟,配合retry.DelayType(retry.BackOffDelay)才触发指数退避 - 必须传
context.Context,否则无法响应超时或取消,retry.Context(ctx)不能少
自定义重试条件:只对特定错误重试,其他直接失败
不是所有错误都该重试。比如 sql.ErrNoRows 是业务正常结果,而 io.EOF 或 net.OpError 才值得等一等。手动判断错误类型比用字符串匹配更安全。
使用场景:调用外部 HTTP 接口时,仅对 5xx 和连接类错误重试,4xx(如 404、400)直接返回。
立即学习“go语言免费学习笔记(深入)”;
- 用
retry.RetryIf(func(err error) bool { return isTransientError(err) })包一层判断函数 - 避免写
strings.Contains(err.Error(), "timeout")—— 错误信息可能变,且中文环境会崩 - 推荐用类型断言或
errors.Is(err, xxx),例如errors.Is(err, context.DeadlineExceeded)不重试
指数退避参数调不好,反而让问题更糟
退避不是越“陡”越好。默认公比是 2,初始 100ms,三次后就是 400ms —— 看似合理,但在高并发下可能堆积大量 pending 请求。生产环境建议压测后调低公比或加 jitter。
性能影响:无 jitter 的纯指数退避会导致“重试风暴”,多个协程在同一时刻发起请求,放大下游压力。
- 加随机抖动:
retry.DelayType(retry.RandomDelay)或自定义函数返回base * (2^attempt) * (0.5 ~ 1.5) - 设上限:
retry.MaxDelay(2 * time.Second),防止单次等待过久拖慢整体响应 - 别用
time.Sleep手算间隔 —— 容易忽略时钟偏移和调度延迟,retry包内部用time.AfterFunc更准
HTTP 客户端集成重试时,记得克隆 request
HTTP 请求体(*http.Request.Body)是一次性的。重试时若没重新生成 Body,第二次就会读到 EOF,返回空数据或 panic。
使用场景:POST JSON 到第三方 API,网络抖动导致失败,需要重发。
- 不要复用原始
req,每次重试前用http.NewRequestWithContext(...)新建,或提前把 payload 缓存为字节切片 - 如果用了
req.Body = ioutil.NopCloser(bytes.NewReader(payload)),确保payload是可重复读的 - 第三方 HTTP 客户端如
resty内置重试,但默认不重放 Body,得显式开.SetRetryCondition(...)并配.SetRequestRawBody(true)
重试逻辑看着简单,真正难的是判断“该不该重试”和“重试到哪一步为止”。尤其是 Body 复用、上下文传播、错误分类这三块,线上出问题时往往不是重试没生效,而是重试把错的请求反复发了十遍。










