如何在Golang中实现错误重试机制_Golang失败重试处理方法

该自己写重试逻辑当需针对特定HTTP状态码、临时网络错误或自定义业务错误(如ErrRateLimited)控制重试时机;Go标准库不自动重试4xx/5xx响应,所有重试必须显式编码。

什么时候该自己写重试逻辑,而不是用第三方库

Go 标准库不提供通用重试机制,net/httpClient.Transport 仅对底层连接失败做有限重试(如 DNS 解析失败、TLS 握手失败),但不会重试 HTTP 4xx/5xx 响应。如果你需要对特定状态码(如 503 Service Unavailable)、临时网络错误(如 io.EOFnet.OpError)或自定义业务错误(如 ErrRateLimited)重试,就得自己控制流程。

常见误判是:看到 http.ClientTimeout 就以为它会自动重试失败请求——它不会。所有重试必须显式编码。

backoff.Retry + 自定义判定函数最稳妥

社区最成熟的选择是 github.com/cenkalti/backoff/v4,它封装了指数退避、抖动、最大重试次数等细节,且允许你决定“什么算失败”。关键不是重试动作本身,而是「何时停止重试」的判定逻辑。

  • 必须显式返回 backoff.Permanent(err) 终止重试(比如遇到 400 Bad Request401 Unauthorized
  • 对可恢复错误(如 503context.DeadlineExceedednet.ErrClosed)直接返回原错误,让 backoff 继续尝试
  • 避免在重试函数里做状态变更(如修改全局变量、写文件),否则重复执行会引发副作用
func callWithRetry(ctx context.Context, url string) error {
    return backoff.Retry(func() error {
        req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            // 网络层错误,可重试
            return err
        }
        defer resp.Body.Close()

        if resp.StatusCode >= 500 || resp.StatusCode == 429 {
            // 服务端临时错误,重试
            return fmt.Errorf("unexpected status: %d", resp.StatusCode)
        }
        if resp.StatusCode >= 400 {
            // 客户端错误,不重试
            return backoff.Permanent(fmt.Errorf("client error: %d", resp.StatusCode))
        }
        return nil
    }, backoff.WithContext(backoff.NewExponentialBackOff(), ctx))
}

time.Sleep 手写重试容易漏掉的关键点

手动用 for + time.Sleep 写重试看似简单,

但极易出错。最容易被忽略的是上下文取消和超时穿透:

  • 没检查 ctx.Err() 就 Sleep,会导致 goroutine 卡死在休眠中,无法响应取消
  • time.Sleep 放在循环开头,可能刚进循环就超时,却仍要 Sleep 一次
  • 退避时间没做抖动(jitter),多个实例同时重试会加剧后端压力
  • 错误类型判断太宽泛(比如对所有 error != nil 都重试),可能把 json.UnmarshalTypeError 这类不可恢复错误也重试

如果坚持手写,至少保证每次 Sleep 前都 select 等待 ctx:

func manualRetry(ctx context.Context, fn func() error) error {
    var err error
    bo := backoff.NewExponentialBackOff()
    for i := 0; i < 3; i++ {
        err = fn()
        if err == nil {
            return nil
        }
        if backoff.Permanent(err) != nil {
            return err
        }
        select {
        case <-time.After(bo.NextBackOff()):
        case <-ctx.Done():
            return ctx.Err()
        }
    }
    return err
}

HTTP 客户端重试必须设置 Request.Body 可重放

这是 Go HTTP 重试里最隐蔽的坑:*http.RequestBodyio.ReadCloser,一旦读取就无法重放。如果原始请求带 JSON body,第二次重试会发送空 body,导致后端报错(如 400 Bad Request)。

解决方法只有两个:

  • 传入可重放的 Body:用 bytes.NewReader 包装原始字节,或用 strings.NewReader
  • 重试前重新构造 Request,确保每次调用 http.NewRequest 都基于原始数据

别依赖 req.Clone(ctx) —— 它不会重置已读取的 Body,除非你事先设了 req.GetBody