Files
learning-go-for-sre-interview/09-safe-counter/main.go
T
kacerrandClaude Fable 5 7219eb5a23 Initial commit: Go SRE interview exercises
Includes solved 02-worker-pool and 09-safe-counter (mutex, atomic, sharded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbnJj9ZqEx9nUvzkjdwfMq
2026-08-28 22:18:09 +02:00

132 lines
3.0 KiB
Go

// Exercise 09 — Concurrency-Safe Counters (three ways)
//
// ASSIGNMENT
// ----------
// A warm-up screener that interviewers run with -race. Implement THREE
// versions of a keyed counter and one plain counter:
//
// 1. MutexCounter: map[string]int64 guarded by a sync.Mutex.
// Inc(key), Get(key), Snapshot() (a COPY of the map — why a copy? if you
// return the internal map, the caller can race on it).
// 2. AtomicCounter: a single int64 total using sync/atomic only.
// Inc(), Get(). No mutex allowed.
// 3. ShardedCounter: N shards, each its own mutex+map, key routed by hash.
// Same interface as MutexCounter. This is how you fix mutex contention
// on hot maps.
//
// Run the tests: go test -race ./09-safe-counter/
// Then benchmark: go test -bench=. -cpu=1,4,8 ./09-safe-counter/
package main
import (
"fmt"
"hash/fnv"
"sync"
"sync/atomic"
)
// ---- 1. Mutex version ----
type MutexCounter struct {
mu sync.Mutex
m map[string]int64
}
func NewMutexCounter() *MutexCounter {
return &MutexCounter{
m: make(map[string]int64),
}
}
func (c *MutexCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[key]++
}
func (c *MutexCounter) Get(key string) int64 {
c.mu.Lock()
defer c.mu.Unlock()
return (c.m[key])
}
func (c *MutexCounter) Snapshot() map[string]int64 {
c.mu.Lock()
defer c.mu.Unlock()
// possibly could use maps.clone
outmap := make(map[string]int64, len(c.m))
for k, v := range c.m {
outmap[k] = v
}
return outmap
}
// ---- 2. Atomic version ----
type AtomicCounter struct {
n atomic.Int64
}
func NewAtomicCounter() *AtomicCounter { return &AtomicCounter{} }
func (c *AtomicCounter) Inc() { c.n.Add(1) }
func (c *AtomicCounter) Get() int64 { return c.n.Load() }
// ---- 3. Sharded version ----
const shards = 16
type shard struct {
mu sync.Mutex
m map[string]int64
}
type ShardedCounter struct {
s [shards]shard
// route with fnv-1a: hash(key) % shards (hash/fnv or hand-rolled)
}
func NewShardedCounter() *ShardedCounter {
c := &ShardedCounter{}
// loop over shards and initialize each shard's map
for i := 0; i < shards; i++ {
c.s[i].m = make(map[string]int64)
}
return c
}
func (c *ShardedCounter) Inc(key string) {
// find shard for the key
hashF := fnv.New32a()
hashF.Write([]byte(key))
shard := hashF.Sum32() % shards
c.s[shard].mu.Lock()
defer c.s[shard].mu.Unlock()
// fmt.Println("shard", shard, "key", key, "value", c.s[shard].m[key])
c.s[shard].m[key]++
}
func (c *ShardedCounter) Get(key string) int64 {
// find shard for the key
hashF := fnv.New32a()
hashF.Write([]byte(key))
shard := hashF.Sum32() % shards
c.s[shard].mu.Lock()
defer c.s[shard].mu.Unlock()
return c.s[shard].m[key]
}
func (c *ShardedCounter) Snapshot() map[string]int64 {
out := make(map[string]int64)
for i := 0; i < shards; i++ {
c.s[i].mu.Lock()
for k, v := range c.s[i].m {
out[k] = v
}
c.s[i].mu.Unlock()
}
return out
}
func main() {
c := NewMutexCounter()
c.Inc("requests")
c.Inc("requests")
fmt.Println(c.Get("requests"), c.Snapshot())
}