错误写法会引发goroutine泄漏、超时失效和重试风暴;正确做法是用backoff.WithContext或retry.Do封装,结合jitter、context取消、状态隔离及shouldRetry判断瞬时故障。

别用裸 for + time.Sleep 包一层 goroutine,那不是重试,是制造 goroutine 泄漏和超时失效的快捷方式。
为什么 go func() { for { ... time.Sleep(...) } }() 是错的
这种写法看着简单,实则三处致命:它完全无视 ctx.Done(),哪怕父 context 已 cancel,goroutine 还在 sleep;每次重试共享同一个计数器,间隔会越算越乱;没有 jitter,多个请求会在同一毫秒醒来,直接触发重试风暴。
- 错误现象:
context deadline exceeded后仍执行 3 轮重试,最后卡死 - 根本原因:
time.Sleep是阻塞调用,无法响应 channel 关闭 - 替代方案:必须用
backoff.WithContext(b, ctx)或retry.Do封装,它们内部用select监听ctx.Done()和退避定时器
用 backoff.Retry 做带上下文感知的重试
这是 Go 社区最稳的实践。它把 jitter、最大重试次数、context 取消、状态隔离全收口了,你只管传策略和操作函数。
- 必须显式构造退避实例:
b := &backoff.ExponentialBackOff{InitialInterval: 100 * time.Millisecond, MaxInterval: 2 * time.Second, MaxElapsedTime: 10 * time.Second} - 必须加 jitter:
b = backoff.WithJitter(b),否则所有重试时间对齐,压垮下游 - 必须包一层
backoff.WithContext(b, ctx),裸调backoff.Retry会忽略取消 - 操作函数里要检查
ctx.Err(),尤其在 HTTP 请求前——别等http.Get自己超时才退出
在 errgroup.Group 里安全重试单个任务
errgroup.Wait() 等的是所有通过 Go() 提交的 goroutine 完成,包括你在里面手动重试的轮次。如果重试逻辑没显式 return,主流程就永远卡住。
立即学习“go语言免费学习笔记(深入)”;
- 重试必须闭环在单个
g.Go(func() error { ... })内部,不能靠外层循环反复g.Go() - 每次重试前都要检查
ctx.Err(),失败后要return err,成功后要return nil - 遇到
panic必须recover()并转为error:return fmt.Errorf("panic during retry: %v", r),否则整个errgroup崩溃 - 别把
backoff.BackOff实例声明为全局变量或结构体字段——每个任务要 new 一个,或至少调b.Reset()
最容易被忽略的一点:重试不是兜底逻辑,而是针对「瞬时故障」的补救。HTTP 400、gRPC codes.InvalidArgument、SQL sql.ErrNoRows 这类错误重试毫无意义,只会放大问题。真正的重试边界,得靠 shouldRetry(err) 统一收口判断,而不是靠 err != nil 一刀切。


















