应优先使用 github.com/cenkalti/backoff/v4 等成熟库而非手写退避逻辑,因其支持 context 取消、jitter、防溢出及 goroutine 安全;复用时须调 b.reset() 并用 backoff.withcontext 包装;初始间隔宜设 100ms;默认策略易致“未重试即超时”;retry.do 比 backoff.retry 更适合业务层,因封装了次数、抖动、错误过滤与 context;http 重试推荐 go-retryablehttp 而非裸 for 循环;grpc 重试需显式注入拦截器且注意错误码范围与消息序列化;核心难点在于错误分类与上下文生命周期管理。

用 github.com/cenkalti/backoff/v4 而不是手写 time.Sleep
直接在 for 循环里写 time.Sleep(1 * time.Second) 或 time.Sleep(time.Duration(math.Pow(2, float64(i))) * time.Second) 是最常见也最危险的起点。它不响应 context 取消、没 jitter、容易整数溢出,而且多个 goroutine 共享同一个退避实例时,NextBackOff() 返回的时间会错乱。
- 每次重试前必须调用
b.Reset()(尤其在复用backoff.BackOff实例时) - 永远用
backoff.WithContext(b, ctx)包一层,别把ctx手动塞进业务函数里传参 - 初始间隔设
100 * time.Millisecond比1 * time.Second更友好——首错就等一秒,用户已经点第二次了 - 别信默认策略:
backoff.NewExponentialBackOff()的MaxInterval=1s和MaxElapsedTime=10s容易导致“重试还没开始就超时”
retry.Do 为什么比 backoff.Retry 更适合业务层
github.com/avast/retry-go 的 retry.Do 封装更厚:它把最大次数、抖动、错误过滤、context 取消全收口了,不用你手动管 Reset() 或拼 WithMaxRetries。而 backoff.Retry 是个裸策略调度器,适合底层 transport 层或需要精细控制的场景。
-
retry.Attempts(3)表示总共执行 3 次(含首次),不是“重试 3 次” -
retry.DelayType(retry.BackOffDelay)必须和retry.Delay(100 * time.Millisecond)配合才生效,单设 delay 没用 - 用
retry.RetryIf(func(err error) bool { return isTransientError(err) })替代字符串匹配,比如别写strings.Contains(err.Error(), "timeout") - 必须传
retry.Context(ctx),否则context.DeadlineExceeded后还会继续重试,goroutine 就卡死了
HTTP 客户端重试该拦在哪一层?
在每个 http.Do 外面套 for 循环,是最难维护、最容易出错的方式。它污染业务逻辑、无法复用、请求体重放(尤其是 POST)容易失败,还容易漏掉 req = req.Clone(req.Context())。
- 推荐用
github.com/hashicorp/go-retryablehttp:它原生支持状态码过滤、jitter、幂等性判断,且保留http.Client的所有行为(redirect、cookiejar 等) -
Client.CheckRetry必须显式定义,比如只对502/503/504和net.OpError重试,4xx错误一律跳过 - 非幂等请求(如 POST)默认不重试,若确定接口幂等,得覆盖
DefaultRetryPolicy并确保req.Body可重放(例如用bytes.NewReader) - 别改
http.DefaultClient—— 健康检查、metrics 上报这类低优先级请求也会被拖进重试,放大下游压力
gRPC 重试为什么配了还不生效?
gRPC 官方客户端重试从 v1.29+ 才正式支持,且默认完全关闭。只配 grpc.WithTimeout 或只加 grpc_retry.WithMax(3),重试逻辑根本不会加载。
- 必须用
grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(...))显式注入拦截器 -
grpc_retry.WithCodes(codes.Unavailable, codes.ResourceExhausted)是安全范围;codes.InvalidArgument或codes.NotFound绝对不能加 - proto.Message 必须可序列化:含
sync.Mutex、func字段或未导出字段会导致重试 panic -
WithMax(2)表示最多发 3 次请求(1 次原始 + 2 次重试),不是“最多重试 2 次”
重试机制真正的复杂点不在算法,而在错误分类和上下文生命周期管理。一个 context.DeadlineExceeded 是该立即返回,还是重置计时器再试一次?这取决于它是来自单次 HTTP 请求超时,还是整个业务流程的 deadline。搞混这两层,退避再准也没用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











