go 中可用高阶函数实现非侵入式自动重试:定义 withretry 接收 func(context.context) error 并返回带指数退避、错误过滤、上下文控制的重试封装函数。

Go 里怎么给函数加自动重试,又不侵入业务逻辑?
直接用装饰器(decorator)模式封装重试逻辑最干净。Go 没有原生装饰器语法,但可以用高阶函数模拟:把 func() 或 func() error 作为参数传进去,返回一个带重试的新函数。
关键不是“怎么写 for 循环”,而是让重试行为和业务解耦——比如你有个 SendEmail(),不该在它内部写 retry logic,而该在外面套一层 WithRetry(SendEmail)。
- 必须接收
func() error类型,否则无法统一判断失败(非 error 返回值没法泛化重试条件) - 重试间隔建议用指数退避,别用固定
time.Sleep(100 * time.Millisecond),否则容易打爆下游 - 别把 context 丢掉——重试函数得接受
context.Context,否则超时/取消控制会失效
WithRetry 函数怎么写才够健壮?
一个可用的 WithRetry 至少要支持:最大重试次数、基础延迟、错误过滤、上下文控制。下面这个版本踩过不少坑:
func WithRetry(fn func(context.Context) error, opts ...RetryOption) func(context.Context) error {
o := defaultRetryOptions()
for _, opt := range opts {
opt(o)
}
return func(ctx context.Context) error {
var err error
for i := 0; i 0 {
select {
case
-
shouldRetry默认只重试net.OpError、context.DeadlineExceeded等临时性错误,**不是所有 error 都该重试**(比如ValidationError重试一百次也没用) - 第 0 次是立即执行,所以循环上限是
,不然会少试一次 - 延迟计算用
1 而不是 <code>math.Pow(2, float64(i)),避免浮点精度和类型转换问题
为什么用 go.uber.org/ratelimit 或 backoff/v4 反而更麻烦?
第三方库如 backoff.Retry 看似省事,但实际容易卡在两个地方:
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
- 它要求你把业务逻辑塞进
func() error闭包里,导致无法复用已有函数签名,尤其当原函数带多个参数时,得手动包装成无参闭包,丢失类型安全 -
ratelimit类库专注限流,不是重试;混用反而让错误处理路径变长(比如先限流再重试,失败原因难归因) - 很多库默认重试所有 error,线上出过因重试
sql.ErrNoRows导致 CPU 打满的事故
真要引入依赖,github.com/cenkalti/backoff/v4 的 RetryNotify + 自定义 BackOff 更可控,但多数场景手写 WithRetry 50 行内搞定,没必要。
HTTP 客户端请求重试,为什么不能只靠 http.Transport 的 MaxIdleConns?
http.Transport 的连接复用和重试是两回事。设了 MaxIdleConns 只影响连接池,**不重试失败的请求**。真正要重试 HTTP 调用,得在业务层做:
- 用
retryablehttp库时,注意它默认只重试 429/5xx,但像 408(Request Timeout)或部分 503 也值得重试,需手动加到Client.RetryCheck - 自己封装时,别在
http.Do外层简单 for 循环——要检查err和resp.StatusCode,有些错误(如url.Error)根本没 resp,直接 panic - GET 请求可安全重试,POST/PUT 必须确认服务端幂等,否则自动重试等于重复提交
重试最难的从来不是代码怎么写,而是判断“这次失败到底能不能重试”。网络超时?重试。参数校验失败?停手。这个决策点藏在 shouldRetry 里,比 for 循环重要十倍。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










