Go语言防止goroutine重复执行 实现连接重连防抖动功能
实现方案
原方案存在的问题
err通道如果持续涌入大量错误,没有做丢弃处理,极端情况会导致发送侧阻塞isDialing仅靠标识位控制,没有覆盖拨号本身的耗时区间,容易出现冷却结束后拨号还在进行时重复触发的问题time.AfterFunc启动的 goroutine 没有绑定生命周期,若函数提前退出会出现 goroutine 泄漏- 未处理
net.Dial本身的失败情况,也没有处理向w通道写入时可能出现的阻塞场景
优化后实现代码
func keepConnUp(n netAddr, w chan<- io.Writer, err chan error) { addr := fmt.Sprintf("%s:%d", n.Addr, n.Port) // 冷却定时器,初始为nil不会触发 var coolDownTimer *time.Timer // 标记是否正在执行拨号逻辑,原子变量避免并发竞争 var isDialing atomic.Bool defer func() { if coolDownTimer != nil { coolDownTimer.Stop() } }() for { select { case <-err: // 已经在拨号/冷却中就直接丢弃当前错误信号,不处理 if isDialing.Load() || (coolDownTimer != nil && time.Until(coolDownTimer.C) > 0) { continue } // 启动冷却计时 coolDownTimer = time.NewTimer(reconnectTimer) case <-coolDownTimer.C: // 标记进入拨号状态 isDialing.Store(true) // 执行拨号,增加超时避免长期卡住 conn, err := net.DialTimeout(n.Network, addr, 5*time.Second) if err != nil { // 拨号失败自动触发下一次冷却重连 coolDownTimer.Reset(reconnectTimer) isDialing.Store(false) continue } // 非阻塞写入w通道,避免接收侧阻塞导致本函数卡住 select { case w <- conn: default: // 写入失败关闭多余连接避免资源泄漏,可自行补充日志逻辑 _ = conn.Close() } isDialing.Store(false) } } }
核心特性说明
- 所有错误信号如果处于冷却期/拨号期直接丢弃,完全不会阻塞
err通道的发送侧,适配大量错误涌入的场景 - 用
atomic.Bool替代普通布尔变量做状态标识,避免并发读写的竞争问题 - 拨号逻辑增加超时控制,避免拨号卡住长期占用状态
- 定时器统一管理,退出时自动停止避免资源泄漏
- 拨号失败自动重置冷却定时器,不需要依赖额外错误信号触发下一次重试
内容的提问来源于stack exchange,提问作者Ado Ren
相关产品推荐
相关产品推荐

