go中可用高阶函数实现通用重试装饰器:定义retryfunc类型,接收context.context、重试策略结构体(maxretries、backoff等),每次循环开头select监听ctx.done(),仅对error返回值判定重试,避免硬编码和goroutine泄漏。

Go 里怎么写一个通用的重试装饰器函数
Go 没有原生装饰器语法,但可以用高阶函数模拟:传入一个 func() 或带参数/返回值的函数,返回一个“自动重试”的新函数。关键不是套壳,而是控制重试时机、错误判定和退出条件。
常见错误是把重试逻辑硬编码进业务函数里,导致每个 HTTP 调用、DB 查询都重复写 for + time.Sleep + 错误判断,既难测又难改。
实操建议:
- 定义重试函数类型,比如
type RetryFunc func() error,统一入口;返回值含 error 才能判断是否重试 - 把重试策略(次数、间隔、退避)抽成结构体参数,别用魔法数字;例如
MaxRetries: 3、Backoff: time.Second - 必须支持上下文(
context.Context),否则超时或取消时无法中断重试循环 - 不要在重试函数里直接
panic或 log,让调用方决定失败后怎么处理
为什么 retry.Do 容易卡死或忽略 context.Cancel
很多开源库(比如 github.com/avast/retry-go)的 retry.Do 默认不接收 context.Context,或者只在第一次调用前检查,后续重试中不监听 cancel —— 这会导致 goroutine 泄漏或用户按 Ctrl+C 后程序不退出。
使用场景:长轮询、K8s client watch、依赖外部服务的 CLI 工具,都需要及时响应中断。
实操建议:
- 自己实现时,每次循环开头加
select { case - 避免用
time.Sleep硬等,改用time.AfterFunc或timer := time.NewTimer并在 cancel 时timer.Stop() - 如果底层函数本身支持
context.Context(如http.Client.Do),务必把外层 ctx 透传进去,不能只控制重试层
重试时怎么区分「该重试」和「该放弃」的错误
不是所有 error 都值得重试。比如 sql.ErrNoRows 是业务正常态,io.EOF 往往表示连接已关,而 net.OpError 中的临时性 DNS 失败才该重试。盲目重试会让问题更糟。
参数差异:Go 的 error 没有分类接口,得靠类型断言或字符串匹配,但后者脆弱。
实操建议:
- 优先用类型判断:
if _, ok := err.(net.Error); ok && err.(net.Error).Temporary() { ... } - 对 HTTP 场景,检查状态码比检查 error 字符串更可靠:
resp.StatusCode >= 500 && resp.StatusCode - 提供自定义判断函数参数,比如
ShouldRetry: func(err error) bool { ... },比内置规则更灵活 - 注意:
errors.Is(err, io.ErrUnexpectedEOF)可用于判断某些协议级临时错误
goroutine 泄漏和并发重试的坑
用 go f() 启动重试逻辑?危险。没控制并发数 + 没回收 done channel = goroutine 积压。尤其在 HTTP handler 里滥用,几秒内就能吃光 GOMAXPROCS。
性能影响:每次重试新建 goroutine 成本不高,但不设上限时,100 个并发请求 × 3 次重试 = 300 个 goroutine,还可能互相抢占调度器。
实操建议:
- 重试必须串行执行(默认行为),除非你明确需要并发探测多个端点;别为“快”牺牲可控性
- 如果真要并发重试(比如 fallback 到备用 API),用
errgroup.Group控制并发 + 共享 context - 所有 timer、channel、goroutine 必须有明确生命周期;
defer清理不如一开始就不用它们 - 测试时加
runtime.GC()和pprof.Lookup("goroutine").WriteTo观察泄漏
最常被忽略的是:重试函数里调用了另一个也带重试的函数,形成嵌套重试。没有全局重试计数或 context 跨层传递的话,会指数级放大延迟和资源占用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











