推荐使用 github.com/cenkalti/backoff/v4 实现带指数退避的错误重试,需传入 context.Context、设置 MaxElapsedTime 和 MaxInterval,并确保业务函数幂等。

用 backoff.Retry 实现带退避的错误重试
Go 标准库不内置重试逻辑,直接手写容易漏掉指数退避、最大重试次数、上下文取消等关键点。推荐用 github.com/cenkalti/backoff/v4 —— 它把重试策略和执行解耦得比较干净。
常见错误是把重试逻辑硬塞进业务函数里,导致测试困难、超时控制失效、goroutine 泄漏。正确做法是让重试器只负责“什么时候重试”,业务函数只返回 error。
- 必须传入
context.Context,否则无法响应外部取消(比如 HTTP 请求被客户端断开) - 退避策略优先选
backoff.NewExponentialBackOff(),别用固定间隔——服务抖动时固定重试会加剧雪崩 -
MaxElapsedTime和MaxInterval都要设,前者防无限重试,后者防退避时间过长(默认 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()。
立即学习“go语言免费学习笔记(深入)”;
- 不要在循环里直接
time.Sleep(d),改用time.AfterFunc或select+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.Limiter的Wait会阻塞,务必传入带超时的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 当成可重试,结果把参数校验失败也重试了三次。










