go 45 lines · 8 steps

A concurrent counter with sync.Map in Go

A per-key counter that stays correct under many goroutines by combining sync.Map with atomic integer operations.

Explained by highlit
1type Counter struct {
2 counts sync.Map
3}
4 
5func (c *Counter) Add(key string, delta int64) int64 {
6 val, _ := c.counts.LoadOrStore(key, new(int64))
7 return atomic.AddInt64(val.(*int64), delta)
8}
9 
10func (c *Counter) Inc(key string) int64 {
11 return c.Add(key, 1)
12}
13 
14func (c *Counter) Get(key string) int64 {
15 val, ok := c.counts.Load(key)
16 if !ok {
17 return 0
18 }
19 return atomic.LoadInt64(val.(*int64))
20}
21 
22func (c *Counter) Snapshot() map[string]int64 {
23 out := make(map[string]int64)
24 c.counts.Range(func(k, v any) bool {
25 out[k.(string)] = atomic.LoadInt64(v.(*int64))
26 return true
27 })
28 return out
29}
30 
31func TallyEvents(events <-chan string, workers int) map[string]int64 {
32 c := &Counter{}
33 var wg sync.WaitGroup
34 for i := 0; i < workers; i++ {
35 wg.Add(1)
36 go func() {
37 defer wg.Done()
38 for key := range events {
39 c.Inc(key)
40 }
41 }()
42 }
43 wg.Wait()
44 return c.Snapshot()
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Pairing sync.Map for key lifecycle with atomic ops for value mutation gives lock-free per-key counting.
  2. 2LoadOrStore guarantees exactly one shared counter per key even when goroutines race to create it.
  3. 3A WaitGroup lets you fan out identical workers over a channel and safely snapshot only after all finish.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A concurrent counter with sync.Map in Go — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code