应使用 backoff.Retry 而非手写 for+time.Sleep,因其内置退避策略、jitter、context 集成与状态隔离;HTTP/gRPC 重试须区分错误类型,幂等性与并发隔离必不可少,且需分层控制超时。

用 backoff.Retry 而不是手写 for + time.Sleep
Go 标准库不提供重试封装,但自己写 for 循环加 time.Sleep 极易出错:阻塞 goroutine、忽略 context 取消、没抖动(jitter)、越界溢出、共享状态错乱。社区事实标准是 github.com/cenkalti/backoff/v4,它把退避策略、随机扰动、最大重试、上下文集成全包了。
- 别直接调
backoff.NewExponentialBackOff()—— 它默认base=100ms、maxInterval=1s,容易在第 4 次重试就卡死(因为间隔不再增长) - 每次复用
backoff.BackOff实例前,必须调b.Reset(),否则NextBackOff()返回的间隔会错乱(内部 attempt 计数器是状态化的) - 必须用
backoff.WithContext(b, ctx)包一层,而不是在操作函数里自己处理ctx.Done();否则重试循环本身无法被取消 - 初始间隔设
100 * time.Millisecond比1 * time.Second更合理:首错不拖太久,也给下游留出恢复窗口
HTTP 请求重试必须区分错误类型,不能无脑 retry
重试不是兜底,而是针对「临时性失败」的补救。对 400 Bad Request、401 Unauthorized、422 Unprocessable Entity 这类客户端错误重试,只会加重服务压力;而 502/503/504、net.OpError、context.DeadlineExceeded(注意:这是上一次请求超时,新请求要重置 context)才值得重试。
- 用
backoff.RetryNotify替代Retry,在通知回调里打日志或提前跳过:if isClientError(err) { return err } - HTTP 场景下,显式检查
resp.StatusCode >= 500,别依赖err != nil—— 有些 4xx 也会返回非空err,但不该重试 - gRPC 错误必须用
status.Code(err)判断,不能strings.Contains(err.Error(), "DeadlineExceeded")—— 字符串匹配脆弱且不可靠 - POST 请求默认不重试(非幂等),若接口确定幂等,需在
CheckRetry或包装函数中显式放行
并发请求必须隔离退避状态,避免多个 goroutine 共享一个 backoff.BackOff
每个请求的退避过程是独立的:第 1 次失败等 100ms,第 2 次等 200ms……这个计数器如果被多个 goroutine 共用,就会出现 A 请求第 3 次失败却拿到 B 请求的第 1 次间隔,结果重试节奏全乱。
- 在 HTTP client 封装层,为每次请求 new 一个
backoff.ExponentialBackOff实例,或至少调b := backoff.NewExponentialBackOff(); b.Reset() - 不要把
backoff.BackOff声明为全局变量或结构体字段长期持有 - 如果用
retryablehttp.Client,它内部已做隔离,但要注意Client.RetryMax是 per-request 的,不是全局计数器 - 测试时禁用真实 sleep:把延迟逻辑抽成
delayFunc func() time.Duration参数传入,方便 mock
超时控制必须分层,外层 context 和单次请求 timeout 不能混用
常见错误是给整个重试流程设一个短 context.WithTimeout(ctx, 500*time.Millisecond),但指数退避本身需要多次尝试 —— 第一次失败后还没来得及 sleep 就触发 context.DeadlineExceeded,根本没机会重试。
立即学习“go语言免费学习笔记(深入)”;
- 外层
context控制「整体截止时间」,内层用独立 timeout 控制单次请求,比如http.NewRequestWithContext(context.WithTimeout(ctx, 200*time.Millisecond), ...) - 退避间隔只占单次请求 timeout 的一部分,建议 base 设
50ms,最大重试 5 次,总耗时大概率压在 500ms 内 - 别把
time.AfterFunc或time.Ticker塞进重试循环 —— 它们不感知ctx.Done(),cancel 后定时器还在跑,可能触发 panic 或无效执行 - 真正可中断的等待,只能靠
select { case 或 <code>backoff.WithContext底层的timer := time.NewTimer()
最常被忽略的点是:抖动(jitter)不是锦上添花,而是上线必选项。没有随机偏移的指数退避,在高并发下必然引发重试风暴 —— 所有实例在同一毫秒发起请求,直接压垮刚恢复的服务节点。


















