重试需控制总耗时、指数退避、区分可重试错误;fallback须独立context、校验数据、打日志上报;并发下用singleflight防重复;所有逻辑必须可观测。

重试不是加个 for 循环就完事
Go 里直接用 for + time.Sleep 实现重试,十有八九会卡死或超时失控。根本问题是没控制总耗时、没退避策略、没区分错误类型。
比如调用下游 HTTP 接口返回 503 Service Unavailable,重试有意义;但如果是 400 Bad Request,重多少次都是错的。
- 必须用指数退避(
backoff.Retry或golang.org/x/time/rate配合) - 总超时要独立于单次请求超时:
context.WithTimeout(ctx, 10*time.Second)包住整个重试流程 - 只对可重试错误重试:检查
err是否为网络错误(net.ErrClosed、url.Error中的Timeout())、HTTP 状态码 429/5xx - 别在
http.Client的Timeout字段上叠加重试——那是单次请求上限,和重试逻辑无关
回退(fallback)不是写个 default 分支就行
很多同学把 fallback 当成“兜底 return”,结果发现服务一降级,缓存没更新、监控没上报、甚至 fallback 返回了过期数据还被当成正常响应。
典型错误是 fallback 和主逻辑共用同一个 context,导致主流程超时 cancel 后,fallback 还在傻等 DB 查询。
立即学习“go语言免费学习笔记(深入)”;
- fallback 必须用新
context.WithTimeout启动,且超时时间要明显短于主流程(比如主流程 800ms,fallback 限 200ms) - fallback 返回前务必校验数据有效性:比如从 Redis 读缓存,得检查
ttl > 0且结构未损坏,否则宁可返回 error - 每次触发 fallback 必须打日志并上报指标:
metrics.Counter("service.fallback_triggered").Inc() - 不要在 fallback 里再调第三方——它本身就得是轻量、本地、确定性高的逻辑(如内存默认值、本地配置、预加载缓存)
并发请求下重试 + 回退的竞态很隐蔽
当多个 goroutine 并发调同一资源(比如查用户余额),各自重试+fallback,容易出现:缓存击穿、DB 重复扣款、fallback 返回不一致数据。
这不是逻辑 bug,是并发控制缺失。Go 的 sync.Once 或简单互斥锁都不够——它们只防初始化,不防请求洪峰。
- 对共享资源访问,必须加请求级去重:用
singleflight.Group,同一 key 的并发请求只执行一次主逻辑,其余等待结果或 fallback -
singleflight的Do方法返回的是原始 error,不是 fallback 结果,别误以为它自动兜底 - 如果主逻辑失败触发 fallback,
singleflight不会自动缓存 fallback 结果——得自己用sync.Map或 Redis 显式缓存,并设短 TTL - 注意
singleflight的 key 是string,别用 struct 直接fmt.Sprintf拼,字段顺序/空格稍变就失效
别让重试放大雪崩,也别让 fallback 欺骗监控
线上最怕的不是失败,是失败后行为不可见。重试次数打满却没告警,fallback 静默生效却没指标,等于给系统埋雷。
Go 的 http.Transport 默认 MaxIdleConnsPerHost=100,但重试一多,连接池可能被占满,新请求直接卡在 dial 阶段,错误日志里连 status 都看不到。
- 所有重试逻辑必须暴露可观察项:当前重试次数(
retry_count标签)、是否最终 fallback(fallback_used布尔)、主路径耗时分位值 - HTTP 客户端要配
Transport.MaxConnsPerHost和Transport.IdleConnTimeout,避免重试堆积连接 - fallback 不是“成功”,它的响应码应明确设为
503或自定义206 Partial Content,并加 headerX-Fallback: true,方便网关/监控识别 - 别在 defer 里做 fallback 日志——万一 panic,defer 不执行,错误就消失了










