应使用 backoff.retry 而非手写 for+time.sleep,因其支持 context 取消、jitter 打散重试风暴、状态隔离、自动防溢出;需每次请求新建退避实例并调用 reset() 和 withcontext(),且仅对临时性错误(5xx、net.operror 等)重试。

别手写 for + time.Sleep,它在 context 超时后仍会 sleep、共享退避计数器、无 jitter、无法响应取消 —— 线上高危。
用 backoff.Retry 而不是裸循环
社区事实标准是 github.com/cenkalti/backoff/v4,它把指数退避、jitter、context 取消、最大耗时、状态隔离全收口了。你只管传策略和操作函数,不用操心 2^i 溢出、计数器错乱或定时器泄漏。
常见错误现象:
-
context.DeadlineExceeded后还在 sleep 并重试 - 多个请求复用同一个
backoff.BackOff实例,第 3 次失败却拿到第 1 次的间隔 - 所有 goroutine 在 400ms 时刻集中发起请求,压垮刚恢复的服务节点
实操建议:
- 每次复用前必须调
b.Reset(),否则NextBackOff()返回值会串号 - 必须用
backoff.WithContext(b, ctx)包一层,裸调backoff.Retry(op, b)完全无视 cancel - 别直接用
backoff.NewExponentialBackOff():默认MaxInterval = 1 * time.Second,第 4 次就卡死在 1s 不再增长
必须加 WithJitter() 才算生产可用
纯指数退避(100ms → 200ms → 400ms)在并发场景下必然形成“重试风暴”。WithJitter() 让等待时间随机偏移 ±25%,打散节奏,这是硬性要求,不是可选项。
性能影响:
- 不加 jitter 的退避,本地测不出问题,上线后高并发下失败率反而飙升
- v4 版本启用 jitter 只需一行:
bo = backoff.WithJitter(bo) - 抖动范围建议 [0.5, 1.5),避免偏移过大拖慢 SLA
示例:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
bo := &backoff.ExponentialBackOff{
InitialInterval: 100 * time.Millisecond,
MaxInterval: 2 * time.Second,
MaxElapsedTime: 10 * time.Second,
}
bo = backoff.WithJitter(bo)
err := backoff.Retry(func() error {
return doHTTPRequest(ctx, url)
}, backoff.WithContext(bo, ctx))
错误分类必须前置,不能靠 err != nil 判断
HTTP 503 和 400 都返回非空 err,但前者该重试、后者该立刻失败。重试不是兜底,而是针对临时性失败(如 net.OpError、context.DeadlineExceeded)的补救。
使用场景:
- 只对
5xx状态码、连接类错误(net.OpError)、gRPCcodes.Unavailable重试 -
400、401、sql.ErrNoRows、codes.InvalidArgument这类错误重试毫无意义,只会放大下游压力
实操建议:
- 封装统一判断函数
shouldRetry(err error) bool,收口逻辑,别散落在各处 - 用
errors.Is(err, context.DeadlineExceeded)或类型断言,别用strings.Contains(err.Error(), "timeout") - HTTP 场景下显式检查
resp.StatusCode >= 500,而非依赖err != nil
每个请求必须隔离退避状态
退避过程是状态化的:Attempt 计数器、当前间隔、是否已达 Stop,都绑定在单个 backoff.BackOff 实例上。多个 goroutine 共享一个实例,就会出现 A 请求第 3 次失败却拿到 B 请求第 1 次的间隔 —— 重试节奏全乱。
实操建议:
-
backoff.BackOff实例不能声明为全局变量或结构体字段长期持有 - 每个请求应独立初始化:
bo := backoff.NewExponentialBackOff(); bo.Reset() - 如果用
retryablehttp.Client,它内部已做隔离,但要注意Client.RetryMax是 per-request 的,不是全局计数器
最容易被忽略的一点:HTTP 请求体(*http.Request.Body)是一次性的。重试时若没重新生成 Body,第二次就读到 EOF —— 这和退避策略无关,但常和重试逻辑一起暴露出来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










