go 60 lines · 8 steps

How a debouncer coalesces bursts in Go

A mutex-guarded timer collapses rapid triggers into a single delayed call, running only the latest work.

Explained by highlit
1package editor
2 
3import (
4 "context"
5 "sync"
6 "time"
7)
8 
9type Debouncer struct {
10 mu sync.Mutex
11 delay time.Duration
12 timer *time.Timer
13 pending func(context.Context) error
14 onError func(error)
15}
16 
17func NewDebouncer(delay time.Duration, onError func(error)) *Debouncer {
18 return &Debouncer{delay: delay, onError: onError}
19}
20 
21func (d *Debouncer) Trigger(fn func(context.Context) error) {
22 d.mu.Lock()
23 defer d.mu.Unlock()
24 
25 d.pending = fn
26 
27 if d.timer != nil {
28 d.timer.Stop()
29 }
30 d.timer = time.AfterFunc(d.delay, d.fire)
31}
32 
33func (d *Debouncer) fire() {
34 d.mu.Lock()
35 fn := d.pending
36 d.pending = nil
37 d.timer = nil
38 d.mu.Unlock()
39 
40 if fn == nil {
41 return
42 }
43 
44 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
45 defer cancel()
46 
47 if err := fn(ctx); err != nil && d.onError != nil {
48 d.onError(err)
49 }
50}
51 
52func (d *Debouncer) Flush() {
53 d.mu.Lock()
54 if d.timer != nil {
55 d.timer.Stop()
56 d.timer = nil
57 }
58 d.mu.Unlock()
59 d.fire()
60}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Debouncing keeps only the most recent request and resets the clock on every new trigger.
  2. 2A mutex around shared timer and pending state makes the debouncer safe to call from concurrent goroutines.
  3. 3Capturing then nil-ing shared state before doing work lets the callback run outside the lock.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How a debouncer coalesces bursts in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code