功能说明
文件监听、输入框编辑和配置变更会在短时间内产生大量事件,如果每次都执行保存或重新加载,既浪费资源又可能读到中间状态。防抖的思路是每次新事件到来就取消上一个定时器,重新计时;只有安静超过设定间隔后才真正执行一次。与节流不同,防抖保证一段连续事件只执行最后一次。
本例的 Debouncer 为每个实例维护一个互斥锁和定时器。Do 在锁内停止旧定时器并创建新的 AfterFunc,Stop 用于关闭时取消待执行任务。演示连续五次调用,每次间隔十毫秒,最终只打印一次触发时间。
关键代码
Debouncer 的 timer 字段由 mu 保护,Do 和 Stop 都先加锁,避免并发调用时同时停止和替换定时器造成竞态。time.AfterFunc 在独立 goroutine 中执行回调,因此回调不能假设自己持有锁。每次 Do 都覆盖 timer,旧回调即使已经进入运行队列也无法取消,所以回调应设计为幂等。main 用 WaitGroup 等待唯一一次触发,start 记录起始时间,输出可验证延迟约为五十毫秒。
使用注意
防抖会延迟执行,不适合要求每次事件都落盘的审计日志。进程退出前应调用 Stop 并等待正在执行的回调,否则最后一次变更可能丢失。若回调可能 panic,应在内部恢复,避免整个进程退出。共享 Debouncer 时,所有调用共享同一个延迟窗口,不同业务键应使用不同实例。Stop 只停止尚未触发的定时器,不能中断已经开始的回调。对高频事件还应限制最大等待时间,防止持续流量导致永远不执行。
验证方式
运行脚本,五次调用后只输出一行 fired after,时间约为一百毫秒,即四次十毫秒的输入间隔加上五十毫秒静默期。把循环内的休眠改为六十毫秒,每次调用都会触发,应连续输出五行,说明间隔超过防抖窗口时不会合并。
代码示例
package main
import (
"fmt"
"sync"
"time"
)
type Debouncer struct {
mu sync.Mutex
delay time.Duration
timer *time.Timer
}
func NewDebouncer(delay time.Duration) *Debouncer {
return &Debouncer{delay: delay}
}
func (d *Debouncer) Do(fn func()) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.delay, fn)
}
func (d *Debouncer) Stop() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
}
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
debounce := NewDebouncer(50 * time.Millisecond)
start := time.Now()
for index := 0; index < 5; index++ {
debounce.Do(func() {
fmt.Println("fired after", time.Since(start).Round(time.Millisecond))
wg.Done()
})
time.Sleep(10 * time.Millisecond)
}
wg.Wait()
}