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

什么时候该自己写重试逻辑,而不是用第三方库
Go 标准库不提供通用重试机制,net/http 的 Client.Transport 仅对底层连接失败做有限重试(如 DNS 解析失败、TLS 握手失败),但不会重试 HTTP 4xx/5xx 响应。如果你需要对特定状态码(如 503 Service Unavailable)、临时网络错误(如 io.EOF、net.OpError)或自定义业务错误(如 ErrRateLimited)重试,就得自己控制流程。
常见误判是:看到 http.Client 有 Timeout 就以为它会自动重试失败请求——它不会。所有重试必须显式编码。
用 backoff.Retry + 自定义判定函数最稳妥
社区最成熟的选择是 github.com/cenkalti/backoff/v4,它封装了指数退避、抖动、最大重试次数等细节,且允许你决定“什么算失败”。关键不是重试动作本身,而是「何时停止重试」的判定逻辑。
- 必须显式返回
backoff.Permanent(err)终止重试(比如遇到400 Bad Request或401 Unauthorized) - 对可恢复错误(如
503、context.DeadlineExceeded、net.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 写重试看似简单,但极易出错。最容易被忽略的是上下文取消和超时穿透:
立即学习“go语言免费学习笔记(深入)”;
- 没检查
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.Request 的 Body 是 io.ReadCloser,一旦读取就无法重放。如果原始请求带 JSON body,第二次重试会发送空 body,导致后端报错(如 400 Bad Request)。
解决方法只有两个:
- 传入可重放的
Body:用bytes.NewReader包装原始字节,或用strings.NewReader - 重试前重新构造
Request,确保每次调用http.NewRequest都基于原始数据
别依赖 req.Clone(ctx) —— 它不会重置已读取的 Body,除非你事先设了 req.GetBody。










