答案是必须用RetryPolicy结构体封装权重,通过Weight缩放BaseDelay实现差异化退避,而非硬编码或概率计算;所有字段需导出以支持JSON热更新,重试调度器须闭包捕获策略并每次动态读取atomic权重快照,避免goroutine丢失上下文或整数溢出。

如何用结构体封装带权重的重试策略
直接在http.Client或grpc.Dial里硬编码重试次数和间隔,根本没法按任务重要性差异化控制。真正可复用的封装,必须把「权重」变成策略的一部分,而不是靠外部if判断跳过重试。
核心是定义一个RetryPolicy结构体,字段包含MaxAttempts、BaseDelay、Multiplier,再加上Weight——注意这个Weight不是用来算概率的,而是决定退避曲线陡峭程度:高权任务容忍更短总等待时间,低权任务允许拉长重试窗口避免挤占资源。
-
Weight建议用int64,值域控制在1–100,避免浮点运算引入误差 - 别把
Weight和MaxAttempts直接相乘——那会导致权重1的任务重试0次;正确做法是用Weight缩放BaseDelay,比如delay = policy.BaseDelay * time.Duration(110-policy.Weight) - 所有字段必须导出(首字母大写),否则无法被
json.Unmarshal反序列化,配置热更新就失效
为什么用time.AfterFunc实现重试会丢权重上下文
常见错误是写go time.AfterFunc(delay, f)然后在f里再调用自身——这相当于把重试逻辑扔进新goroutine,原始任务的Weight信息彻底丢失,后续重试全部退化为等权行为。
正确做法是让重试调度器本身持有策略实例,并在每次触发时显式传入当前权重参数。最简方案是用闭包捕获policy和weight:
立即学习“go语言免费学习笔记(深入)”;
func (r *RetryScheduler) Schedule(task func() error, policy RetryPolicy, weight int64) {
var attempt int
var lastErr error
var run func()
run = func() {
if attempt >= policy.MaxAttempts {
r.onFailure(lastErr)
return
}
if err := task(); err != nil {
lastErr = err
delay := calculateDelay(policy.BaseDelay, policy.Multiplier, attempt, weight)
time.AfterFunc(delay, run)
}
attempt++
}
run()
}注意calculateDelay必须接收weight参数,且不能依赖全局变量——否则并发调用时不同任务的weight会互相覆盖。
重试间隔计算怎么避免整数溢出和精度坍塌
用time.Second * 2^attempt这种指数退避,当attempt超过30就溢出int64;而用float64做乘法再转time.Duration,又会在小数点后三位开始丢失精度,导致第5次重试和第6次间隔几乎一样。
- 固定用
time.Duration做运算,避免float转换:例如delay := policy.BaseDelay ,但需加<code>if attempt > 30 { delay = maxDelay }兜底 - 权重参与计算时,别用
delay *= weight——这会让权重100的任务延迟翻100倍;推荐用delay += time.Duration(weight) * time.Millisecond * 50这种线性偏移 - 所有延迟值必须通过
time.Duration类型检查,禁止直接传int给time.AfterFunc,Go 1.22+会panic
如何让重试策略支持运行时权重变更
任务执行中途权重可能因业务状态变化(如用户VIP等级提升),但已启动的time.AfterFunc回调无法修改。解决方案不是取消重试,而是让每次重试前重新读取权重快照。
关键是在run闭包里每次调用task前,都从一个线程安全的位置获取最新weight:
- 用
sync/atomic存int64型权重,比sync.RWMutex轻量得多 - 不要把
weight存在task函数闭包里——那是只读快照;得存在独立变量,由业务层随时atomic.StoreInt64(&w.weight, newWt) - 每次重试前调用
atomic.LoadInt64(&w.weight),然后传给calculateDelay,这样第3次重试就能响应权重下调
真正的难点不在代码长度,而在所有重试路径是否都经过同一套权重读取逻辑——漏掉任何一个分支,就会出现“看起来改了权重,但某些失败请求还在用旧值重试”的现象。


















