go 59 lines · 8 steps

A thread-safe sliding-window average in Go

Track a running average over a time window by evicting samples that fall outside it, all under a mutex.

Explained by highlit
1package metrics
2 
3import (
4 "sync"
5 "time"
6)
7 
8type sample struct {
9 value float64
10 at time.Time
11}
12 
13type SlidingAverage struct {
14 mu sync.Mutex
15 window time.Duration
16 samples []sample
17 sum float64
18 now func() time.Time
19}
20 
21func NewSlidingAverage(window time.Duration) *SlidingAverage {
22 return &SlidingAverage{
23 window: window,
24 now: time.Now,
25 }
26}
27 
28func (s *SlidingAverage) Add(v float64) {
29 s.mu.Lock()
30 defer s.mu.Unlock()
31 
32 now := s.now()
33 s.samples = append(s.samples, sample{value: v, at: now})
34 s.sum += v
35 s.evict(now)
36}
37 
38func (s *SlidingAverage) Average() float64 {
39 s.mu.Lock()
40 defer s.mu.Unlock()
41 
42 s.evict(s.now())
43 if len(s.samples) == 0 {
44 return 0
45 }
46 return s.sum / float64(len(s.samples))
47}
48 
49func (s *SlidingAverage) evict(now time.Time) {
50 cutoff := now.Add(-s.window)
51 i := 0
52 for i < len(s.samples) && s.samples[i].at.Before(cutoff) {
53 s.sum -= s.samples[i].value
54 i++
55 }
56 if i > 0 {
57 s.samples = append(s.samples[:0], s.samples[i:]...)
58 }
59}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Maintaining a running sum alongside the sample slice makes the average O(1) instead of re-summing every call.
  2. 2Injecting a now func() time.Time makes time-dependent code deterministic and testable.
  3. 3Guarding every read and write with the same mutex keeps the sum and slice consistent under concurrent access.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A thread-safe sliding-window average in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code