← 中文资源

简体中文版本

合并高频调用的防抖器

用 time.AfterFunc 与互斥锁实现防抖:连续事件只触发一次延迟函数,适合保存草稿、搜索建议和配置重载。

阅读英文原文

功能说明

文件监听、输入框编辑和配置变更会在短时间内产生大量事件,如果每次都执行保存或重新加载,既浪费资源又可能读到中间状态。防抖的思路是每次新事件到来就取消上一个定时器,重新计时;只有安静超过设定间隔后才真正执行一次。与节流不同,防抖保证一段连续事件只执行最后一次。

本例的 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()
}