超时控制必须用 context.WithTimeout,每次重试新建带超时的 context 并调用 cancel;重试需分类错误类型,区分网络/5xx(可重试)与 4xx/业务错误(多数不重试);退避应使用指数退避加 jitter,推荐 backoff/v4 库;轮询必须带唯一 task_id 并保证幂等写入。

超时控制必须用 context.WithTimeout,不是 time.After
直接用 time.After 做超时会泄漏 goroutine,尤其在重试场景下极易堆积。正确做法是每次重试都新建带超时的 context.Context,让底层 HTTP 客户端或 RPC 调用能感知并主动中断。
常见错误是把一个 long-lived context 传进整个重试循环——一旦第一次超时,后续重试仍共享已取消的 context,全部立即失败。
- 每次重试前调用
ctx, cancel := context.WithTimeout(parentCtx, timeout) - 务必在重试结束(无论成功或失败)后调用
cancel() - 若使用
http.Client,需显式传入该 ctx 到client.Do(req.WithContext(ctx))
重试策略要区分错误类型:网络抖动 vs 业务拒绝
盲目对所有错误重试,可能把 400 Bad Request 或 401 Unauthorized 也重试三次,既无意义又加重服务压力。真正的容错需要分类响应。
典型区分方式:
立即学习“go语言免费学习笔记(深入)”;
- 底层连接错误(
net.OpError、url.Error)、5xx 状态码 → 可重试 - 4xx 中除 429(Too Many Requests)外多数不重试;429 需配合
Retry-Afterheader 解析退避 - 自定义业务错误(如
ErrTaskNotFound)应直接返回,不纳入重试逻辑
建议用函数类型封装判断逻辑:shouldRetryFunc := func(err error, statusCode int) bool { ... }
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
退避时间不能写死,要用 backoff.Retry 或指数退避
固定间隔重试(如每次都等 1s)在分布式高并发下容易引发雪崩。真实轮询必须引入 jitter 和指数增长。
Go 生态推荐直接用 github.com/cenkalti/backoff/v4,它内置了 ExponentialBackOff 并自动加随机抖动,避免同步重试风暴。
- 不要自己手写
time.Sleep(time.Second * time.Duration(i*i)) - 设置
MaxInterval防止退避时间过长(例如超过 30s 就不再退避,改用固定上限) - 注意:backoff 库的重试函数默认不处理 context 超时,需手动 wrap 进
backoff.WithContext
分布式轮询必须带唯一任务 ID + 幂等写入
多个 worker 同时轮询同一任务,若没做幂等控制,可能多次触发下游动作(如发两次短信)。这不是重试机制能解决的问题,而是设计前提。
关键点:
- 轮询请求必须携带服务端可识别的
task_id,且该 ID 全局唯一(推荐 ULID 或时间戳+机器号) - 每次轮询结果写入(如标记“已完成”)必须是原子的、带条件的(例如 SQL 的
UPDATE ... WHERE status = 'pending') - 如果用 Redis,用
SET task:123 done NX EX 3600确保仅首次写入生效
漏掉幂等性,再严谨的超时重试也只是在重复制造错误。

















