time.Sleep 直接写在 for 循环里错误,因无退避机制,连续重试易压垮下游或触发限流;应使用指数退避(如 backoff/v4),配合 context 控制超时,并按错误类型精准判断是否重试。

为什么 time.Sleep 直接写在 for 循环里是错的
因为没有退避(backoff),连续重试会压垮下游或触发限流。比如调用一个不稳定的 HTTP 接口,立刻重试 5 次,和等 100ms → 200ms → 400ms 再试,对服务端压力完全不同。
- 固定间隔重试(如每次
time.Sleep(100 * time.Millisecond))适合已知故障恢复极快的场景,但多数网络抖动需要指数退避 - 别手写指数计算:容易溢出、没上限、忽略 jitter(随机扰动)。直接用
github.com/cenkalti/backoff/v4的ExponentialBackOff -
backoff.WithMaxRetries和backoff.WithContext必须配着用,否则超时控制失效 —— 单纯设context.WithTimeout不会中断正在 sleep 的 goroutine
HTTP 请求重试时,哪些错误能重试、哪些必须放弃
不是所有 error 都该重试。Go 的 net/http 错误类型混杂,得靠错误值判断,而不是看字符串是否含 “timeout” 或 “connection”。
- 可安全重试:
url.Error且err.Unwrap()是*net.OpError,且Op == "dial"或Op == "read";http.ErrHandlerTimeout(注意:这是 handler 超时,不是 client) - 绝对不要重试:
http.ErrUseLastResponse(重定向循环)、status >= 400 && status (客户端错误,比如 401/404)、<code>url.Error中Err是*url.Error(URL 解析失败) - 检查方式别用
strings.Contains(err.Error(), "timeout")—— 容易漏判或误判,改用errors.Is(err, context.DeadlineExceeded)或errors.As(err, &net.OpError{})
用 retryablehttp 库时,Request.Retryable 字段为什么经常被忽略
这个字段决定单次请求是否参与重试流程,但它默认是 true,所以很多人根本没意识到它存在 —— 直到某次 POST 请求被意外重发两次。
- GET/HEAD 请求默认可重试,但 POST/PUT/DELETE 默认也
true,这不符合幂等性原则。务必手动设req.Retryable = false,除非你确认后端支持幂等 -
retryablehttp.Client的CheckRetry函数里,不能只看状态码,还得检查req.Retryable—— 否则自定义逻辑会被绕过 - 如果用了中间件(比如加签名),记得在重试前重新生成签名,否则第二次请求可能因时间戳/nonce 失效被拒
自己封装重试函数时,context.Context 传参位置很关键
重试逻辑里最容易漏掉的是把原始 ctx 传进每次重试的子操作,而不是只传给最外层的 for 循环。
立即学习“go语言免费学习笔记(深入)”;
- 错误写法:
for i := 0; i —— 这里 <code>doSomething()完全收不到 cancel 信号 - 正确做法:每次重试都用
ctx, cancel := context.WithTimeout(parentCtx, oneTryTimeout),并在 defer 里cancel();或者用context.WithDeadline控制总耗时 - 如果重试函数接收
func(ctx context.Context) error,就别在内部再套context.WithTimeout—— 让调用方控制超时更灵活
重试的边界感比想象中难把握:超时怎么分、错误怎么分、上下文怎么传,三个点只要一个没对齐,就可能出现“以为重试了三次,其实只试了一次”或者“重试五次后才发现 context 已 cancel”的情况。










