重试必须结合错误类型过滤和抖动退避:永久性错误(如sql.ErrNoRows)应标记为不可重试,临时性错误才重试;指数退避需加jitter避免重试风暴;每次重试须新建context并检查取消信号。

重试次数不能只靠 maxRetries 硬控,必须配合错误类型过滤
直接设 maxRetries = 3 并不等于“最多试 3 次”,因为永久性错误(比如 sql.ErrNoRows、json.SyntaxError、os.IsNotExist)不该进重试循环。否则会浪费次数、掩盖真实问题。
常见错误分类方式:
- 临时性错误:重试有意义,如
net.OpError、context.DeadlineExceeded、HTTP 状态码503/429 - 确定性错误:重试无意义,如
sql.ErrNoRows、strconv.ParseInt失败、errors.Is(err, os.ErrPermission)
用 backoff.Permanent(err) 显式标记不可重试错误;手写循环时用 errors.Is 或类型断言判断,别只看 err != nil。
间隔策略不是越长越好,指数退避必须加 jitter
纯指数退避(1s → 2s → 4s)在并发场景下容易导致“重试风暴”——大量请求在同一时刻撞上服务端,反而压垮它。抖动(jitter)是必须项,不是可选项。
立即学习“go语言免费学习笔记(深入)”;
抖动推荐做法:
- 每次延迟在
[base * 0.75, base * 1.25]范围内随机,上限不超过 0.5 倍基础值 - 用独立的
*rand.Rand实例,避免多 goroutine 共享默认 seed 导致抖动失效 - 别用
math/rand.Float64()直接套公式,要先初始化:r := rand.New(rand.NewSource(time.Now().UnixNano()))
例如第 2 次重试(n=1),基础间隔为 2s,则实际 sleep 在 1.5s~2.5s 之间随机,而非固定 2s。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
context 取消和超时必须每次重试都新建
很多人把一个 ctx 传进重试循环,结果第二次重试时 ctx.Err() != nil 已成立,但没检查就继续执行,造成无效调用或资源泄漏。
正确姿势:
- 每次循环开头调用
context.WithTimeout(parentCtx, timeout)新建子 context - 立刻
defer cancel()或显式cancel(),尤其在循环内不能依赖 defer(会累积) - 在
select中监听ctx.Done(),一旦触发立即跳出,不 sleep、不重试
错误示例:err := backoff.Retry(fn, backoff.WithMaxRetries(b, 3)) 却传入已 cancel 的 ctx —— 它不会自动感知取消,最后一次重试可能卡死。
用 backoff.Retry 还是手写 for 循环?看场景复杂度
第三方库 github.com/cenkalti/backoff/v4 不是银弹,但能帮你避开多数坑:自动 jitter、解耦策略与执行、支持 Permanent 错误标记、兼容 context。
手写循环只适合这些情况:
- CLI 工具、单元测试辅助函数等低频、单次调用场景
- 逻辑极简,且你能保证覆盖三件事:检查
ctx.Err()、区分错误类型、控制退避计算
生产服务中,只要涉及 HTTP、gRPC、数据库连接等外部依赖,优先用 backoff.Retry + backoff.WithMaxRetries + backoff.WithJitter 组合。自己算 1 和 <code>time.Sleep 容易漏掉边界条件,比如最后一次不该 sleep。

















