backoff.retry需配合错误分类、上下文取消和超时控制才能安全用于http重试,否则会重试永久错误、忽略cancel信号或导致goroutine泄漏。

backoff.Retry 是当前 Go 生态中最稳妥的重试入口,但直接套用会踩坑——它不自动过滤错误、不感知上下文取消、也不限制总耗时。真正在框架里落地容错,得拆开看策略、执行、判定三块。
为什么不能直接用 backoff.Retry 包住 HTTP 请求
很多人把 http.Client.Do 一包就完事,结果线上发现:503 重试了,400 也重试了,甚至 JSON 解析失败都重试三次。这不是容错,是添乱。
关键问题在于:backoff.Retry 默认对任何 error 都重试,而 HTTP 场景下只有部分状态码和底层错误值得 retry:
-
5xx和429可重试,4xx(除408、429外)一律跳过 -
net.OpError、io.EOF、context.DeadlineExceeded属于临时错误;sql.ErrNoRows、json.SyntaxError是永久错误,必须用backoff.Permanent标记 - 没传
context.Context的话,哪怕超时了,重试还在继续,goroutine 就卡死了
手写重试循环时最容易漏掉的三件事
不引入第三方库时,for + sleep 看似简单,但生产环境出问题基本都栽在这三处:
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
- 每次
time.Sleep前没select等待ctx.Done(),导致 cancel 信号被忽略 - 最后一次尝试后多睡了一次——正确做法是
if i - 错误判断只用
err != nil,没做类型断言或errors.Is,比如把os.ErrNotExist当临时错误重试
for i := 0; i <h3> <code>backoff.NewExponentialBackOff()</code> 必须调的两个配置</h3><p>直接 new 出来就用,大概率让重试间隔失控。默认 <code>MaxInterval</code> 是 1 分钟,<code>MaxElapsedTime</code> 是 0(即不限总耗时),这在 API 调用中完全不可接受:<br></p>
-
bo.MaxInterval = 500 * time.Millisecond—— 防止单次等待太久,用户已放弃 -
bo.MaxElapsedTime = 3 * time.Second—— 从第一次尝试开始计时,超时即停,比单纯数次数更合理 - 别忘了每次复用 backoff 实例前调
bo.Reset(),否则退避状态会跨请求污染
重试函数里最危险的副作用
框架层封装重试时,如果业务函数本身有状态变更(比如扣库存、发消息),重试等于重复执行——除非你保证幂等。
常见疏忽点:
- 没校验请求 ID 或加唯一 key 去重,导致下游重复消费
- 重试时重新生成
*http.Request,但意外复用了同一个req.Body(io.ReadCloser只能读一次) - 在重试闭包里捕获了外部变量并修改,造成并发竞态
重试不是兜底,是选择性自救。错在哪类错误上重试、重试多久、是否允许并发触发——这些决策点藏在参数细节和错误分类里,不在循环语法中。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










