必须用 backoff.WithContext(b, ctx) 包装退避实例,否则重试忽略 context 取消信号;退避实例需隔离或重置;避免默认指数退避参数过小;仅对临时性错误重试并统一判断逻辑;抖动为必需项;POST 重试须服务端幂等支持。

为什么不能直接用 backoff.Retry
它完全忽略 context.Context 的取消信号。哪怕外层 ctx 已被 cancel() 或超时,重试仍会跑完全部次数,最终卡死 goroutine。常见现象是日志里反复出现 context deadline exceeded,但程序还在 sleep 并尝试第 4、第 5 次重试。
必须用 backoff.WithContext(b, ctx) 包装退避实例,让每次 NextBackOff() 都主动检查 ctx.Done()。别把 ctx 手动塞进业务函数参数里传——容易漏判、难统一、易误写成 context.Background()。
backoff.BackOff 实例必须隔离或重置
每个请求应独立初始化退避实例,或复用时严格调用 b.Reset()。共享实例会导致 attempt 计数器串号,比如 A 请求第 2 次失败却拿到 B 请求第 1 次的间隔(100ms),而本该是 200ms。
别直接用 backoff.NewExponentialBackOff():它的 MaxInterval = 1 * time.Second 太小,第 4 次重试就不再增长,实际等待时间卡死在 1s。推荐显式构造:
立即学习“go语言免费学习笔记(深入)”;
bo := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5) bo.MaxInterval = 30 * time.Second bo.InitialInterval = 100 * time.Millisecond
初始间隔设 100 * time.Millisecond 比 1 * time.Second 更合理——首错不拖太久,也给下游留出恢复窗口。
只对临时性错误重试,且必须用 errors.Is 判断
HTTP 场景下,只重试 net.OpError、context.DeadlineExceeded(注意:这是上一次请求的超时,新请求要重置 ctx),以及 resp.StatusCode >= 500 || resp.StatusCode == 429。4xx 错误如 400、401、422 一律不重试,否则放大下游压力。
gRPC 场景下,用 status.Code(err) 判断,只重试 codes.Unavailable、codes.ResourceExhausted,跳过 codes.NotFound、codes.InvalidArgument。
避免 strings.Contains(err.Error(), "timeout"):错误信息可能本地化、服务端变更,且无法覆盖 net/http.ErrHandlerTimeout 这类服务端超时。封装统一函数 shouldRetry(err error) bool 收口逻辑,别散落在各处。
抖动(jitter)不是可选项,是必须项
纯 2^n 退避在高并发下会形成脉冲流量,瞬间压垮刚恢复的服务节点。抖动范围建议取 [0.5, 1.5):
delay := time.Duration(float64(base) * math.Pow(2, float64(attempt))) * (0.5 + rand.Float64()*0.5)
每个请求需独立初始化 *rand.Rand,别用全局 rand.Intn——并发修改 rand.Source 会 panic。POST 请求默认不重试(非幂等),若服务端明确保证幂等,才在 shouldRetry 中放行。
真正容易被忽略的点是:重试前提醒幂等性须服务端配合。客户端再怎么指数退避、熔断降级,只要后端不认这个 key,重试就是危险操作。


















