重试没生效最常见原因是错误类型判断不当:要么太宽松(如仅用err != nil)导致非重试错误被重试,要么太严格(如字符串匹配超时)漏掉真实超时错误;应使用errors.Is或errors.As精准识别错误类型。

为什么重试没生效,却看不出错在哪
最常见的情况是:重试逻辑写了,但实际根本没触发。原因通常是错误类型判断太宽松或太严格——比如只检查 err != nil 就重试,结果把 400 Bad Request 也重试了;或者用 strings.Contains(err.Error(), "timeout") 判断超时,但某些底层错误不带这个字符串,直接跳过重试。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 先确认你捕获的是哪类错误:如果是
*url.Error,用errors.As(err, &net.OpError{})提取底层net.OpError,再看Op字段是否为"dial"或"read" - 对上下文超时,必须用
errors.Is(err, context.DeadlineExceeded),而不是靠字符串匹配 - HTTP 状态码错误(如
503)不会产生err != nil,它走的是resp.StatusCode分支,得单独判断 - 打印原始错误类型:
fmt.Printf("err type: %T, err: %+v\n", err, err),比看日志更准
重试后 goroutine 卡住不退出怎么办
典型表现是:请求已超时或被取消,但重试循环还在 sleep,甚至发出了第 4、5 次请求。根源在于没在每次重试前检查 ctx.Err(),导致上下文信号被忽略。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个重试周期开头加
select { case ,不能只在循环外检查一次 - 别把同一个
context.Context传给所有重试——每次重试应新建子 context,例如childCtx, cancel := context.WithTimeout(ctx, timeoutPerAttempt) - 用
backoff.WithContext(backoff.NewExponentialBackOff(), ctx)替代手写time.Sleep,它会自动响应ctx.Done() - 如果用了
time.AfterFunc或time.NewTimer,记得timer.Stop()防泄漏
POST 请求重试导致数据重复提交
这是幂等性缺失的典型症状。标准库的 http.Client 默认对所有方法都重试,包括 POST,而多数后端不保证 POST 幂等。你看到日志里两次相同请求体,大概率就是这个原因。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 显式设置
req.Retryable = false(如果你用的是retryablehttp库),或在自定义重试判定函数中拦截非幂等方法 - 对必须重试的
POST,确保后端支持幂等 key(如Idempotency-Keyheader),并在每次重试前生成新值 - 中间件(如签名、时间戳)必须在每次重试前重新计算,否则第二次请求因签名失效被拒
- 避免在重试函数里修改全局状态或写文件——重试 ≠ 多次执行业务逻辑
退避策略看起来在动,但压根没缓解下游压力
比如设了指数退避,但监控显示重试请求仍密集打到服务端。问题往往出在退避没真正“生效”:要么间隔太短(100ms 起步在生产环境几乎等于没退避),要么没加抖动,大量请求在退避后同一毫秒发起。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 起始间隔至少设为
500ms,上限不超过2s;总重试次数控制在3次以内 - 手动实现时,用
time.Sleep(time.Duration(1 容易溢出(<code>i=31就超 int32),改用time.Second 或直接用 <code>backoff/v4 - 必须加随机抖动:比如在计算出的休眠时间上
+ rand.Int63n(300) * time.Millisecond - 退避只解决“节奏”问题,不解决“总量”问题——高并发场景下,还得配合限流(如
semaphore控制并发重试数)
重试模块最难调的不是逻辑,而是边界条件:上下文穿透、错误分类粒度、幂等性落地、退避与限流的协同。这些地方稍一松懈,就从“容错”变成“负优化”。


















