直接用 time.sleep 做重试延迟会出问题,因其硬编码休眠时间无法适配不同失败场景,且并发时所有 goroutine 同步阻塞、无法响应上下文取消;应改用 retry.do 配合自定义 backofffunc 实现可中断的调度决策。

为什么直接用 time.Sleep 做重试延迟会出问题
因为硬编码休眠时间无法应对不同失败场景:网络抖动可能 100ms 就恢复,而下游服务重启可能要等 30s。更关键的是,并发调用时所有 goroutine 会同步卡在 time.Sleep,既浪费调度资源,又无法响应上下文取消(ctx.Done() 被忽略)。你得把延迟逻辑从“阻塞等待”变成“可中断的调度决策”。
retry.Do 配合自定义 retry.BackoffFunc 是最简可行路径
Go 生态里 github.com/avast/retry-go 提供了开箱即用的策略抽象。它不强制你写循环,而是把“下次该等多久”这个判断抽成函数:
func exponentialBackoff(attempt uint) time.Duration {
base := time.Millisecond * 100
return time.Duration(float64(base) * math.Pow(2, float64(attempt))) +
time.Duration(rand.Int63n(int64(base)))
}
使用时只需传入该函数:
err := retry.Do(
func() error { return apiCall() },
retry.Context(ctx),
retry.DelayType(exponentialBackoff),
retry.MaxDelay(30*time.Second),
)
-
retry.DelayType接收func(uint) time.Duration,参数是当前重试次数(从 0 开始) - 返回值为正数时,引擎会在该延迟后发起下一次尝试;返回 0 表示立即重试;负数会被截断为 0
- 注意:
retry.MaxDelay是兜底限制,防止指数爆炸——比如第 10 次重试理论延迟超 100 秒,但实际最多等 30 秒
需要精确控制 jitter 或退避上限?自己实现 BackoffFunc 更可靠
第三方库的 jitter 实现往往固定比例(如 ±30%),但有些场景要求 jitter 必须落在 [50ms, 200ms] 区间内,或希望最大退避锁定在 5s 不受指数影响。这时别依赖配置项,直接写函数:
func boundedJitter(attempt uint) time.Duration {
if attempt == 0 {
return 0 // 首次失败不延迟
}
base := time.Second * 2
max := time.Second * 5
jitter := time.Duration(rand.Int63n(150)) * time.Millisecond // [0ms, 150ms)
delay := base + jitter
if delay > max {
delay = max
}
return delay
}
这种写法绕过了库的内部 jitter 计算逻辑,完全自主可控。重点在于:attempt 参数必须参与计算,否则无法体现“越往后等越久”的语义;且所有时间运算必须用 time.Duration 类型,混用 int 和 time.Millisecond 容易静默溢出。
Context 取消和重试计数必须联动,否则会漏掉关键信号
常见错误是只检查 ctx.Err() 却忽略重试次数已满——导致本该放弃的请求还在空转。正确做法是在 retry.Do 外层加一层判断:
err := retry.Do(
func() error {
select {
case
-
retry.Attempts(3)表示最多执行 3 次(含首次),不是“额外重试 3 次” -
retry.IsMaxRetriesError是类型断言工具函数,比strings.Contains(err.Error(), "max retries")安全得多 - 如果业务要求“重试过程中任何一次 ctx 取消都立刻终止”,就必须在重试函数体内部做
select检查,不能只靠retry.Context的顶层封装
真正的难点不在写策略函数,而在于理清「谁决定终止」「终止时错误归因到哪一层」——这决定了监控告警能否准确定位是下游故障、还是调用方超时配置过短。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











