
Go 里用 context.Context 控制重试的生命周期
重试不是无限循环,而是受上下文约束的有限尝试。一旦 ctx.Done() 被触发(超时、取消),所有重试必须立即终止,否则会泄漏 goroutine 或阻塞调用方。
- 每次重试前都应检查
ctx.Err() != nil,为真就直接返回错误,不进下一次循环 - 不要在重试循环里新建
context.WithTimeout覆盖原始ctx,这会丢失父级取消信号;如需单次重试超时,用context.WithDeadline基于原ctx派生 - 若重试逻辑含 I/O(如 HTTP 请求),确保底层操作本身接收并响应
ctx,比如http.Client的Do(req.WithContext(ctx))
time.Sleep 在重试中怎么加才不翻车
盲目 time.Sleep 会导致重试间隔固定、无法退避、且阻塞当前 goroutine。真正可用的重试等待,必须可中断、可退避、能响应 cancel。
- 用
time.AfterFunc或select+time.After配合ctx.Done(),避免 Sleep 阻塞取消路径 - 简单线性退避不够健壮,推荐用
backoff.Retry(来自github.com/cenkalti/backoff/v4)或手写指数退避:每次等待 =base * 2^attempt,上限设为几秒防雪崩 - 别在重试循环里写
time.Sleep(100 * time.Millisecond)—— 这个值既没考虑上下文,也没退避,还难测试
重试失败后怎么把原始错误和重试次数一起传出去
只返回最后一次失败的错误,等于丢掉了关键诊断信息:是第一次就挂了,还是重试五次后才失败?网络抖动还是服务永久不可用?
- 定义一个包装错误类型,比如
type RetryError struct { Err error; Attempts int; LastErr error },实现Error()和Unwrap() - 不要用字符串拼接错误(
fmt.Errorf("failed after %d attempts: %w", n, lastErr)),这样会丢失原始错误链;用%w包裹LastErr保持可展开性 - 如果调用方要区分“临时错误”和“永久错误”,重试函数内部应在判定是否重试前,先判断错误是否实现了某个接口(如
Temporary() bool),而不是统一重试所有错误
第三方库选 backoff 还是手写?
除非业务对重试行为有极特殊要求(比如按错误码跳过某些重试、动态调整退避系数),否则别手写。社区库已覆盖绝大多数边界:上下文集成、退避策略、重试条件、回调钩子。
立即学习“go语言免费学习笔记(深入)”;
-
github.com/cenkalti/backoff/v4是最常用选择,backoff.Retry默认支持context.Context,且Operation函数签名强制你返回 error,天然适配重试语义 - 避免用
github.com/hashicorp/go-retryablehttp做通用重试 —— 它专为 HTTP 封装,内部耦合了http.Client,没法复用于数据库连接、gRPC 调用等场景 - 如果项目已用
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp,注意它的中间件不自动传播重试指标,需手动在重试循环里打 span
重试逻辑最容易被忽略的,是「什么时候不该重试」—— 400 Bad Request、401 Unauthorized、501 Not Implemented 这类错误,重试一百次也没用。真正的重试边界,得靠错误语义判断,不是靠次数或时间兜底。










