用backoff.retry封装重试逻辑最省心,因其内置jitter、自动响应context取消、支持错误分类与最大重试次数控制,避免手写for+time.sleep遗漏关键逻辑。

用 backoff.Retry 封装重试逻辑最省心
直接写 for + time.Sleep 容易漏掉上下文取消、错误分类和 jitter,backoff.Retry 把这些收口了,你只管传策略和操作函数。它默认带 jitter,避免重试风暴;自动响应 ctx.Done(),不会卡死;支持 backoff.WithMaxRetries 控制总尝试次数。
常见错误现象:time.Sleep(100 * time.Millisecond) 写死导致下游被压垮;没检查 ctx.Err() != nil 就继续重试;把 sql.ErrNoRows 这类确定性错误也重试。
- 必须用
backoff.WithContext(bo, ctx)包一层,否则退避过程无视 cancel -
backoff.NewExponentialBackOff()默认MaxElapsedTime=10s,常误判超时,建议显式构造并设InitialInterval和MaxInterval - 操作函数里别只看
err != nil,要用errors.Is(err, net.OpError{})或类型断言区分可重试错误
自己写 for 循环重试时必须手动处理三件事
不是所有场景都值得引入第三方包,比如 CLI 工具或测试辅助函数。但手写必须显式覆盖这三点,否则大概率出问题。
- 每次循环开头检查
ctx.Err() != nil,立即返回,否则可能忽略 cancel 信号 - 用
time.AfterFunc或select配合ctx.Done()做延迟,不能裸用time.Sleep - 错误判断不能只看
err != nil,得用errors.Is或类型断言:比如os.IsNotExist(err)不该重试,syscall.EIO才该重试
HTTP 请求重试别包装 http.Client,改在 Do 前封装
很多人想给 http.Client 加重试,试图包装 Do 方法,结果发现 Transport 层的连接复用、TLS 握手、redirect 处理全乱了。正确做法是把重试逻辑放在请求构造之后、发送之前。
- 重试粒度必须是“完整请求+响应处理”,不是只重试
client.Do调用 - 对
resp.StatusCode做语义判断:比如503、429可重试,400、401不该重试 - 别在重试循环里重复调用
req.Clone(ctx),否则可能丢失 body 或 header
异步重试用 channel + goroutine 时怎么避免泄漏
起 goroutine 后不等它结束,或者没监听 ctx.Done() 就阻塞在 channel 上,一旦父 context 被 cancel,goroutine 就成僵尸协程。
- 所有阻塞操作必须受 context 约束:sleep 改用
select+time.After,channel 操作必须和ctx.Done()同级select - 别复用同一个
backoff.BackOff实例跨多个 goroutine,要每次新建或调b.Reset() - 如果用
retry.Do(来自github.com/avast/retry-go),注意retry.Attempts(3)表示总共执行 3 次(含首次),不是“最多重试 3 次”
重试模块最难的不是写循环,而是错误分类边界和退避状态隔离——比如 NFS 挂载点临时离线时 os.Open 返回 syscall.EIO,标准库不自动转成语义化错误,不显式识别就永远重试失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











