必须用 backoff.withcontext(b, ctx) 包装,否则重试完全忽略 context 取消;需每次请求新建或重置退避实例、仅对临时性错误(如5xx、net.operror)重试,并启用 jitter 防重试风暴。

直接用 backoff.Retry 是错的,它不感知 context.Context 取消;重试必须绑定上下文、隔离退避状态、区分错误类型,否则大概率出现 goroutine 泄漏、超时后继续执行、或重试风暴。
为什么不能裸用 backoff.Retry
它只接收 func() error 和 backoff.BackOff,完全忽略 ctx.Done()。哪怕外层 context 已被 cancel 或超时,重试仍会跑完全部次数。
- 常见错误现象:
context deadline exceeded后还在 sleep 并重试,最终卡死 goroutine - 正确做法:必须用
backoff.WithContext(b, ctx)包装,让每次NextBackOff()都检查上下文状态 - 别把
ctx手动塞进业务函数参数里传——容易漏掉、难统一、易误写成context.Background()
如何初始化和复用 backoff.BackOff 实例
每个请求应独立初始化退避实例,或复用时严格重置状态;共享实例会导致 attempt 计数器串号、间隔错乱。
- 每次循环重试前必须调
b.Reset(),尤其在 for 循环中复用b时 - 别直接用
backoff.NewExponentialBackOff():它的MaxInterval = 1 * time.Second太小,第 4 次重试就可能被截断 - 推荐显式构造:
b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), 5),再设b.MaxInterval = 30 * time.Second - 初始间隔建议
100 * time.Millisecond,而非1 * time.Second——首错等一秒,用户早点第二次了
哪些错误该重试?怎么判断才可靠
盲目重试等于放大故障。HTTP 400、gRPC codes.InvalidArgument、SQL sql.ErrNoRows 这类错误重试毫无意义,还加重下游压力。
- HTTP 场景:只对
resp.StatusCode >= 500或网络错误(如net.OpError)重试;4xx一律返回不重试 - gRPC 场景:用
status.Code(err)判断,只重试codes.Unavailable、codes.ResourceExhausted,跳过codes.NotFound - 别用
strings.Contains(err.Error(), "timeout"):错误信息可能本地化、服务端变更,且无法覆盖net/http.ErrHandlerTimeout - 封装统一函数:
shouldRetry(err error) bool,收口逻辑,避免散落在各处
抖动(jitter)和并发安全怎么落地
纯 2^n 退避在高并发下会形成脉冲流量,瞬间压垮刚恢复的服务节点。jitter 不是可选项,是必须项。
- 抖动范围建议取
[0.5, 1.5):延迟 =base * math.Pow(2, float64(attempt)) * (0.5 + rand.Float64()*0.5) - 每个请求必须独立初始化
*rand.Rand,别用全局rand.Intn——并发修改rand.Source会 panic -
backoff/v4内置WithJitter,但需手动启用:backoff.WithJitter(b, 0.5) - HTTP 客户端集成重试,别在每个
http.Do外套 for 循环——污染业务、难统一幂等处理、易漏req.Clone(req.Context())
最易被忽略的是错误分类与上下文生命周期管理:一个 context.WithTimeout 创建后,所有子调用(包括 HTTP client、DB query)都必须透传该 ctx,否则底层连接不响应取消;而错误是否临时,必须靠类型判断而非字符串匹配——这两点出错,重试机制就从容错变成负优化。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











