Go 高手(19):定时器与资源管理——time.Ticker、time.Timer、time.After 陷阱
更新时间:2026-09-01。本文是
languages/go/intermediate/高手层第 19 篇,接 基准测试与性能优化。定时器是 Go 并发编程中常用的工具,用得不对就会泄漏资源。time.Ticker不 Stop 会一直运行,time.After在 select 里用要小心,time.Timer的 Reset 和 Stop 有正确用法。
本文要回答的问题
time.Ticker不 Stop 会泄漏吗?怎么正确使用?time.Timer的 Stop 和 Reset 的正确用法是什么?time.After在 select 循环里会泄漏内存吗?为什么?time.NewTicker和time.Tick有什么区别?
一、time.Ticker:周期定时器
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 必须 Stop,否则泄漏
for range ticker.C {
// 每秒执行一次
fmt.Println("tick")
}不 Stop 的后果: Ticker 内部持有定时器资源,不 Stop 会一直运行,直到程序退出。频繁创建 Ticker 不 Stop 会导致资源泄漏。
正确的使用模式: 在创建 Ticker 的地方就 defer ticker.Stop()。
二、time.Ticker 的 Stop 和 Reset
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
// 停止定时器
ticker.Stop() // 停止后 channel 不会关闭,但不会收到新 tick
// 重置周期
ticker.Reset(2 * time.Second) // 从 1 秒改成 2 秒Stop 后,ticker 的 channel 不会关闭,但不会再有新值发送。所以 for range ticker.C 会永远阻塞。推荐用 for 循环 + select 模式:
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 定时任务
case <-done:
return
}
}三、time.Timer:一次性定时器
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
fmt.Println("2 seconds passed")
case <-done:
return
}Timer 的 Stop 和 Reset:
timer := time.NewTimer(5 * time.Second)
// Stop:返回 true 表示定时器在 Stop 前没有触发
if timer.Stop() {
// 定时器未触发,可以安全地 Reset
timer.Reset(10 * time.Second)
}
// Reset 的正确用法:先 Stop,再 Reset
// 只有在定时器已经触发或已经 Stop 时才能 ResetReset 的坑: 如果定时器已经触发(<-timer.C 已经收到了值),但没有 drain channel,直接 Reset 可能导致 Reset 后立刻触发。正确做法是 Reset 前先 Stop。
四、time.After:在 select 里的陷阱
// ❌ 错误:time.After 在循环里会泄漏
for {
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}time.After 内部创建了一个 time.Timer,每次循环都创建一个新的。如果 select 执行了其他 case,定时器没有被 Stop,一直等到触发才释放。循环足够多次,就会累积大量未释放的定时器。
// ✅ 正确:在循环外创建 Timer
timeout := 1 * time.Second
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
timer.Reset(timeout) // 重置定时器
select {
case v := <-ch:
fmt.Println(v)
case <-timer.C:
fmt.Println("timeout")
}
}time.After 长啥样?
func After(d Duration) <-chan Time {
return NewTimer(d).C
}它创建了一个 Timer,但调用者没法 Stop 它。所以在 select 循环里不要用 time.After,用 time.NewTimer + Reset。
五、time.Tick vs time.NewTicker
// time.Tick:创建 Ticker,但调用者无法 Stop
for range time.Tick(1 * time.Second) {
// 无法 Stop,持续到程序退出
}
// time.NewTicker:调用者可以 Stop
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()time.Tick 适合不需要 Stop 的简单场景。time.NewTicker 适合需要控制生命周期的场景。
六、定时器的最佳实践
// 1. 周期性任务的正确模式
func periodicTask(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doTask()
case <-ctx.Done():
return
}
}
}
// 2. 超时控制的正确模式
func doWithTimeout(timeout time.Duration) error {
timer := time.NewTimer(timeout)
defer timer.Stop()
done := make(chan error, 1)
go func() {
done <- doWork()
}()
select {
case err := <-done:
return err
case <-timer.C:
return fmt.Errorf("timeout after %v", timeout)
}
}七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| Ticker 不 Stop | 定时器资源泄漏 | defer ticker.Stop() |
循环里用 time.After | 累积大量定时器,内存泄漏 | 循环外用 time.NewTimer,循环内 Reset |
| Timer Stop 后直接 Reset | Reset 可能立即触发 | 先 Stop,确认返回 true 后再 Reset |
| Timer 触发后不 drain channel | Reset 行为异常 | Stop 后再 Reset,或 drain 掉 channel 值 |
相关与延伸
下一篇:信号处理与优雅关闭——os/signal、Shutdown;time 包的基础用法,见 时间处理。
一句话总结
Go 定时器与资源管理:time.Ticker 必须 Stop,defer ticker.Stop() 确保释放;time.Timer 的 Stop 和 Reset 要配合使用,Stop 返回 true 后再 Reset;time.After 在 select 循环里泄漏,用 time.NewTimer + Reset 替代;time.Tick 没有 Stop 方法,只能用于简单场景;定时器创建后要能 Stop,否则资源泄漏。