指数退避需手动计算等待时间:第n次失败后等待baseDelay

用 time.Sleep 实现指数退避的核心逻辑
指数退避不是靠库“自动给”,而是手动计算每次重试前的等待时间:第 n 次失败后,等待 baseDelay * 2^n,再加点随机抖动防雪崩。Go 标准库没封装这个模式,得自己写循环 + time.Sleep。
关键点是别直接写 time.Sleep(time.Second * time.Duration(math.Pow(2, float64(n))))——math.Pow 返回 float64,精度丢失且慢;改用位移更安全:baseDelay (前提是 <code>baseDelay 是整数毫秒或纳秒)。
- 起始延迟建议设为
100 * time.Millisecond,太小易打满下游,太大拖慢恢复 - 必须加抖动:
time.Sleep(delay + time.Duration(rand.Int63n(int64(jitter)))),jitter通常取delay的 1/3 - 最大重试次数别硬编码,应作为参数传入,避免调用方无法控制
封装成泛型函数时如何处理错误判定
不是所有错误都该重试。比如 http.StatusNotFound 或 validation.ErrInvalidInput 是客户端问题,重试无意义;而 context.DeadlineExceeded、net.OpError、io.EOF 才值得等下次。
所以函数签名里得接收一个 shouldRetry func(error) bool,而不是只看 error != nil。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 别在函数内部写死判断逻辑,否则复用性归零
- 常见误判:把
json.UnmarshalError当网络错误重试——它其实是响应体解析失败,说明服务已返回,不该重试 - 如果调用的是 HTTP 客户端,建议先检查
resp.StatusCode再决定是否进重试循环,别等err != nil才开始判断
用 context.Context 控制重试生命周期
用户可能只想等 5 秒就放弃,不管重试了几次。这时候靠 for i := 0; i 不够,必须结合 <code>ctx.Done()。
典型错误是:先 sleep 再 select ctx,导致最后一次 sleep 还没结束,上下文已超时,但 goroutine 还在等——这会卡住调用方。
- 每次循环开头就
select检查ctx.Done(),有信号立刻返回ctx.Err() - sleep 前也做一次
select,防止刚算完 delay 就超时 - 别用
time.After替代time.Sleep,它会泄漏 timer,尤其高频调用时
完整可抄的最小可用示例
下面这段可以直接粘贴进项目,支持泛型、抖动、自定义重试判断、Context 取消:
func DoWithExponentialBackoff[T any](ctx context.Context, fn func() (T, error), opts ...func(*backoffConfig)) (T, error) {
cfg := &backoffConfig{
baseDelay: 100 * time.Millisecond,
maxRetries: 5,
jitter: 50 * time.Millisecond,
shouldRetry: func(error) bool { return true },
}
for _, opt := range opts {
opt(cfg)
}
<pre class="brush:php;toolbar:false;">var result T
var err error
for i := 0; i <= cfg.maxRetries; i++ {
select {
case <-ctx.Done():
return result, ctx.Err()
default:
}
result, err = fn()
if err == nil {
return result, nil
}
if !cfg.shouldRetry(err) || i == cfg.maxRetries {
return result, err
}
delay := cfg.baseDelay << i
if delay > time.Second {
delay = time.Second // 防止指数爆炸
}
delay += time.Duration(rand.Int63n(int64(cfg.jitter)))
select {
case <-time.After(delay):
case <-ctx.Done():
return result, ctx.Err()
}
}
return result, err}
type backoffConfig struct { baseDelay time.Duration maxRetries int jitter time.Duration shouldRetry func(error) bool }
func WithBaseDelay(d time.Duration) func(backoffConfig) { return func(c backoffConfig) { c.baseDelay = d } }
func WithMaxRetries(n int) func(backoffConfig) { return func(c backoffConfig) { c.maxRetries = n } }
func WithJitter(j time.Duration) func(backoffConfig) { return func(c backoffConfig) { c.jitter = j } }
func WithShouldRetry(f func(error) bool) func(backoffConfig) { return func(c backoffConfig) { c.shouldRetry = f } }
真正容易被忽略的是:抖动值 jitter 和 baseDelay 单位必须一致;maxRetries 包含首次尝试(即最多执行 maxRetries + 1 次),文档不写清楚,调用方极易误解。

















