应使用 retryconfig 结构体封装重试行为,避免裸写 for 循环(如 for i := 0; i
用
RetryConfig结构体封装重试行为,别裸写 for 循环裸写
for i := 0; i 看似简单,但无法响应取消、不能过滤错误、退避策略写死、后续加日志或监控就得动业务代码。真正可维护的重试逻辑必须封装成结构体,把策略和执行解耦。推荐定义一个
RetryConfig,字段至少包含:MaxRetries、BaseDelay、MaxDelay、ShouldRetry(函数类型)、OnRetry(可选回调)。所有重试入口统一走Do(ctx context.Context, fn func() error) error方法。
ShouldRetry必须显式判断错误类型,比如只对net.OpError、syscall.EIO、errors.Is(err, context.DeadlineExceeded)返回 true,而os.IsNotExist或sql.ErrNoRows应立刻退出BaseDelay建议设为100 * time.Millisecond起步——本地 I/O 故障恢复快,没必要从 1s 开始等- 每次重试前必须调用
ctx.Err()检查,已取消就直接返回,否则 goroutine 会卡住HTTP 请求重试必须重建
*http.Request,且用req.WithContext复用同一个
*http.Request是高频翻车点:第二次调用client.Do(req)会报http: Request.Body is nil,因为 body 已被读取或关闭;更隐蔽的是,它携带的原始 context 可能已过期,导致超时失效。正确做法是每次重试都新建请求,并显式注入当前重试轮次的 context:
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
- 别依赖
http.Client.Timeout全局设置,它不参与 context 传播,且无法在重试中动态调整- 状态码判断要分层:5xx、429 可重试;400/401/404 属于语义错误,重试无意义
- 非幂等请求(如 POST)重试前,务必确认服务端支持
Idempotency-Key或其他幂等机制并发重试要用 worker pool 控制 goroutine 数量
直接为每个任务起一个 goroutine 并封装重试逻辑,看似并行,实则危险:1000 个任务就起 1000 个 goroutine,内存暴涨、调度开销大,还可能瞬间打爆下游服务。
Golang Spf13 Viper下载Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
应该用固定数量的 worker(比如 5–20 个),通过 channel 分发任务:
- 启动 N 个长期运行的 goroutine,每个内部跑带重试的
Do方法- 任务入参走
chan Task,结果分别发往successCh chan Result和failCh chan FailInfo- 主流程用
select或range收集结果,避免阻塞- 所有 worker 都监听同一个
ctx,父 context 取消时全部退出指数退避必须加随机抖动,否则容易触发雪崩
纯
sleep = base * 2^n在多实例场景下会导致大量请求在同一时刻重试,形成周期性流量尖峰。真实环境里,必须引入 jitter(抖动)。简单实现就是每次 sleep 前加一个随机偏移,比如 ±25%:
delay := time.Duration(float64(baseDelay) * math.Pow(2, float64(attempt))) * (0.75 + 0.5*rand.Float64()) time.Sleep(delay)
- 抖动系数建议 0.5–0.75,太小没效果,太大削弱退避意义
- 不要用
rand.Intn直接生成毫秒数——它破坏了指数增长趋势,退化成随机等待- 如果用第三方库(如
cenkalti/backoff/v4),直接启用WithJitter即可,不用自己算真正的难点不在“怎么重试”,而在“什么时候不该重试”——错误分类漏判、context 生命周期管理错位、幂等边界模糊,这三个地方出问题,重试引擎反而会放大故障。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!












