应优先使用 time.AfterFunc 实现延迟执行,它轻量、可取消、不阻塞调用方;避免 goroutine + time.Sleep 的误用,因其不可取消、变量捕获风险高、精度差。

直接用 time.AfterFunc 就够了,别自己起 goroutine 包裹 time.Sleep
最常见误用是写 go func() { time.Sleep(3 * time.Second); doSomething() }() —— 这看似“延迟执行”,实则埋下三颗雷:goroutine 无法取消、变量捕获可能读到过期值、time.Sleep 阻塞该 goroutine 且精度差。真正该用的是 time.AfterFunc,它由 runtime timer 系统统一调度,轻量、可取消、不阻塞调用方。
示例:
timer := time.AfterFunc(3*time.Second, func() {
fmt.Println("已延迟执行")
})
// 后续可随时取消
if !timer.Stop() {
// 已触发,跳过
}-
time.AfterFunc返回*time.Timer,必须保存引用才能取消;丢弃返回值 = 放弃控制权 - 闭包里别捕获外部指针或 map/slice 变量,容易读到修改前的快照;如需状态,传值或显式拷贝
- 主 goroutine 退出时,未触发的
AfterFunc会直接丢失——确保程序存活时间 ≥ 延迟时间,比如加select{}或time.Sleep
time.NewTimer + select 是超时控制的唯一可靠组合
当你需要等待某个操作完成,但又不能无限等下去(比如 HTTP 请求、数据库查询),必须用 time.NewTimer 配合 select。这是 Go 超时模式的标准写法,time.Sleep 和裸 AfterFunc 都不适用。
关键点:
立即学习“go语言免费学习笔记(深入)”;
- 创建后立即开始计时,
是阻塞接收,只应读一次;重复读会永久阻塞 - 用完必须调用
timer.Stop(),哪怕已经触发——否则底层 timer 结构可能被复用,导致后续 panic - 如果
select中多个 case 同时就绪(比如 channel 关闭 + timer 到期),Go 随机选一个,逻辑上要能容忍这种不确定性
典型超时结构:
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
<p>select {
case result := <-ch:
handle(result)
case <-timer.C:
log.Println("timeout")
}反复重用 time.Timer 必须检查 Stop() 返回值
高频重置一个 time.Timer(比如每秒更新一次倒计时)时,Reset() 不是安全替代 Stop() 的操作。如果 timer 已触发,Reset() 会往已关闭的 C 通道发值,直接 panic: send on closed channel。
正确流程只能是:
- 先调
timer.Stop(),判断返回值 - 若返回
false,说明 timer 已触发或已被 Stop 过,此时必须timer.Reset()前先timer = time.NewTimer(...) - 若返回
true,说明成功停止,可安全Reset()
简写为:
if !timer.Stop() {
timer = time.NewTimer(newDur)
} else {
timer.Reset(newDur)
}大量动态延迟任务别堆 time.AfterFunc,改用最小堆 + 单 Timer
当你要管理几百个不同到期时间的任务(如订单关单、消息重试),每个都调 time.AfterFunc 会导致 timer 对象爆炸、GC 压力大、无法批量查询或调整。这时得自己实现延迟队列核心:用 container/heap 维护按 DueAt 排序的最小堆,再配一个常驻 goroutine 持续 pop 到期任务。
关键约束:
- 堆元素结构体必须实现
heap.Interface,Less(i, j int) bool必须比较DueAt字段 - 每次堆有变更(插入/删除),都要重设唯一
*time.Timer的触发时间:timer.Reset(earliest.DueAt.Sub(time.Now())) - 堆空时务必
timer.Stop(),否则它会在遥远未来触发一次无意义事件 - pop 后必须调
heap.Pop()或heap.Remove(),否则堆结构错乱,后续Fix()也救不回来
这类实现里最容易被忽略的,是任务函数执行时的 panic 处理——没 recover 的 panic 会让整个延迟队列 goroutine 崩溃,后续所有任务全丢。必须在执行回调前套一层 recover。


















