17370845950

如何在Golang中实现错误重试机制_Golang错误重试与限流控制方法
推荐使用 github.com/cenkalti/backoff/v4 实现带指数退避的错误重试,需传入 context.Context、设置 MaxElapsedTime 和 MaxInterval,并确保业务函数幂等。

backoff.Retry 实现带退避的错误重试

Go 标准库不内置重试逻辑,直接手写容易漏掉指数退避、最大重试次数、上下文取消等关键点。推荐用 github.com/cenkalti/backoff/v4 —— 它把重试策略和执行解耦得比较干净。

常见错误是把重试逻辑硬塞进业务函数里,导致测试困难、超时控制失效、goroutine 泄漏。正确做法是让重试器只负责“什么时候重试”,业务函数只返回 error

  • 必须传入 context.Context,否则无法响应外部取消(比如 HTTP 请求被客户端断开)
  • 退避策略优先选 backoff.NewExponentialBackOff(),别用固定间隔——服务抖动时固定重试会加剧雪崩
  • MaxElapsedTimeMaxInterval 都要设,前者防无限重试,后者防退避时间过长(默认 MaxInterval 是 1 分钟,对多数 API 来说太长)
bo := backoff.NewExponentialBackOff()
bo.MaxElapsedTime = 5 * time.Second
bo.MaxInterval = 500 * time.Millisecond
err := backoff.Retry(func() error {
    _, err := http.Get("https://api.example.com/data")
    return err
}, bo)

手动实现简单重试时如何避免 goroutine 泄漏

有人用 for + time.Sleep 写重试,但没检查 ctx.Done() 就直接 sleep,会导致 goroutine 卡住不退出。这是线上事故高发点。

核心原则:每次 sleep 前都 select 等待 ctx.Done();每次重试前都检查 ctx.Err()

  • 不要在循环里直接 time.Sleep(d),改用 time.AfterFuncselect + time.After
  • 重试间隔建议从 100ms 起步,最多翻 3–4 次,再长用户已感知超时
  • 如果重试函数本身有副作用(如发消息、扣库存),必须保证幂等,否则重试等于多扣
func retryWithCtx(ctx context.Context, fn func() error, maxRetries int) error {
    var err error
    for i := 0; i <= maxRetries; i+

+ { if ctx.Err() != nil { return ctx.Err() } err = fn() if err == nil { return nil } if i == maxRetries { break } select { case <-time.After(time.Duration(i+1) * 100 * time.Millisecond): case <-ctx.Done(): return ctx.Err() } } return err }

重试 + 限流组合使用时的关键顺序

先限流再重试,不是反过来。否则限流器看到的是“重试请求洪峰”,可能直接拒绝所有流量。

典型错误:用 golang.org/x/time/rate.Limiter 包裹整个重试块,导致第一次失败后,后续重试全被限流器挡在门外,实际重试次数远低于预期。

  • 限流应作用于“单次请求尝试”,即每次调用 http.Do 前做 limiter.Wait(ctx)
  • 重试逻辑在限流之后,这样每次重试都是独立受控的请求
  • 注意 rate.LimiterWait 会阻塞,务必传入带超时的 ctx,否则重试整体耗时不可控
limiter := rate.NewLimiter(rate.Every(100*time.Millisecond), 1)
for i := 0; i <= 3; i++ {
    if err := limiter.Wait(ctx); err != nil {
        return err
    }
    resp, err := http.DefaultClient.Do(req)
    if err == nil {
        return nil
    }
    // 失败则继续下一次重试
}

哪些错误不该重试,以及怎么判断

HTTP 400、401、403、404、422 这类客户端错误,重试毫无意义,反而浪费资源。但 Go 的 net/http 不区分错误类型,全塞进 error 接口里。

必须显式解析响应状态码或错误底层原因。别依赖 err.Error() 字符串匹配——不稳定且难维护。

  • *url.Error,检查 err.Unwrap() 是否为 *net.OpError,再看 Timeout()Temporary() 返回值
  • 对 HTTP 响应,读取 resp.StatusCode 后再决定是否重试,4xx 错误直接返回
  • 数据库操作中,sql.ErrNoRows 不是错误,driver.ErrBadConn 才适合重试

最易忽略的一点:重试逻辑里没做错误分类,把所有 error 当成可重试,结果把参数校验失败也重试了三次。