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
This commit is contained in:
+479
@@ -0,0 +1,479 @@
|
||||
# Reference Solutions — SPOILERS
|
||||
|
||||
Don't open this until you've fought the tests yourself. The struggle is the training.
|
||||
|
||||
These are *reference* solutions (verified green under `go test -race`), not the only valid shape — a worker-pool answer to exercise 01 or a hand-rolled list in 05 is equally correct if the tests pass.
|
||||
|
||||
|
||||
<details><summary><b>01-url-health-checker</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
URL string
|
||||
Status int
|
||||
Latency time.Duration
|
||||
Err error
|
||||
}
|
||||
|
||||
func CheckAll(ctx context.Context, urls []string, limit int) []Result {
|
||||
results := make([]Result, len(urls))
|
||||
sem := make(chan struct{}, limit)
|
||||
var wg sync.WaitGroup
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
|
||||
for i, u := range urls {
|
||||
wg.Add(1)
|
||||
go func(i int, u string) {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case sem <- struct{}{}:
|
||||
defer func() { <-sem }()
|
||||
case <-ctx.Done():
|
||||
results[i] = Result{URL: u, Err: ctx.Err()}
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
results[i] = Result{URL: u, Err: err}
|
||||
return
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
results[i] = Result{URL: u, Err: err, Latency: time.Since(start)}
|
||||
return
|
||||
}
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
results[i] = Result{URL: u, Status: resp.StatusCode, Latency: time.Since(start)}
|
||||
}(i, u)
|
||||
}
|
||||
wg.Wait()
|
||||
return results
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>02-worker-pool</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Pool[T, R any](jobs <-chan T, workers int, fn func(T) R) <-chan R {
|
||||
out := make(chan R)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := range jobs {
|
||||
out <- fn(j)
|
||||
}
|
||||
}()
|
||||
}
|
||||
go func() { wg.Wait(); close(out) }()
|
||||
return out
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>03-rate-limiter</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Limiter struct {
|
||||
mu sync.Mutex
|
||||
tokens float64
|
||||
max float64
|
||||
rate float64
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func NewLimiter(rate float64, burst int) *Limiter {
|
||||
return &Limiter{tokens: float64(burst), max: float64(burst), rate: rate, last: time.Now()}
|
||||
}
|
||||
|
||||
func (l *Limiter) Allow() bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
now := time.Now()
|
||||
l.tokens = math.Min(l.max, l.tokens+now.Sub(l.last).Seconds()*l.rate)
|
||||
l.last = now
|
||||
if l.tokens >= 1 {
|
||||
l.tokens--
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>04-log-topn</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type KV struct {
|
||||
Endpoint string
|
||||
Count int
|
||||
}
|
||||
|
||||
func TopN(r io.Reader, n int) ([]KV, error) {
|
||||
counts := make(map[string]int)
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
|
||||
for sc.Scan() {
|
||||
fields := strings.Fields(sc.Text())
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
counts[fields[2]]++
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]KV, 0, len(counts))
|
||||
for k, v := range counts {
|
||||
out = append(out, KV{k, v})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Count > out[j].Count })
|
||||
if len(out) > n {
|
||||
out = out[:n]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>05-lru-cache</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
key string
|
||||
val any
|
||||
}
|
||||
|
||||
type LRU struct {
|
||||
mu sync.Mutex
|
||||
cap int
|
||||
ll *list.List
|
||||
items map[string]*list.Element
|
||||
}
|
||||
|
||||
func NewLRU(capacity int) *LRU {
|
||||
return &LRU{cap: capacity, ll: list.New(), items: make(map[string]*list.Element)}
|
||||
}
|
||||
|
||||
func (c *LRU) Get(key string) (any, bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if el, ok := c.items[key]; ok {
|
||||
c.ll.MoveToFront(el)
|
||||
return el.Value.(*entry).val, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *LRU) Put(key string, val any) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if el, ok := c.items[key]; ok {
|
||||
el.Value.(*entry).val = val
|
||||
c.ll.MoveToFront(el)
|
||||
return
|
||||
}
|
||||
if c.ll.Len() >= c.cap {
|
||||
back := c.ll.Back()
|
||||
c.ll.Remove(back)
|
||||
delete(c.items, back.Value.(*entry).key)
|
||||
}
|
||||
c.items[key] = c.ll.PushFront(&entry{key, val})
|
||||
}
|
||||
|
||||
func (c *LRU) Len() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.ll.Len()
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>06-retry-backoff</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrRetryable = errors.New("retryable")
|
||||
|
||||
func Retry(ctx context.Context, attempts int, base time.Duration, fn func(context.Context) error) error {
|
||||
var err error
|
||||
for i := 0; i < attempts; i++ {
|
||||
if err = fn(ctx); err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, ErrRetryable) {
|
||||
return err
|
||||
}
|
||||
if i == attempts-1 {
|
||||
break
|
||||
}
|
||||
backoff := base << i
|
||||
sleep := time.Duration(rand.Int64N(int64(backoff)))
|
||||
select {
|
||||
case <-time.After(sleep):
|
||||
case <-ctx.Done():
|
||||
return errors.Join(ctx.Err(), err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("after %d attempts: %w", attempts, err)
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>07-fan-in</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func Merge[T any](ctx context.Context, chs ...<-chan T) <-chan T {
|
||||
out := make(chan T)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(chs))
|
||||
for _, ch := range chs {
|
||||
go func(ch <-chan T) {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case out <- v:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}(ch)
|
||||
}
|
||||
go func() { wg.Wait(); close(out) }()
|
||||
return out
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>08-graceful-shutdown</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewServer(addr string, h http.Handler) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: h,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, srv *http.Server, drainTimeout time.Duration) error {
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.ListenAndServe() }()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
shutCtx, cancel := context.WithTimeout(context.Background(), drainTimeout)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(shutCtx); err != nil {
|
||||
return fmt.Errorf("drain: %w", err)
|
||||
}
|
||||
if err := <-errCh; err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
<details><summary><b>09-safe-counter</b></summary>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
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()
|
||||
c.m[key]++
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
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()
|
||||
cp := make(map[string]int64, len(c.m))
|
||||
for k, v := range c.m {
|
||||
cp[k] = v
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
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() }
|
||||
|
||||
const shards = 16
|
||||
|
||||
type shard struct {
|
||||
mu sync.Mutex
|
||||
m map[string]int64
|
||||
}
|
||||
|
||||
type ShardedCounter struct {
|
||||
s [shards]shard
|
||||
}
|
||||
|
||||
func NewShardedCounter() *ShardedCounter {
|
||||
c := &ShardedCounter{}
|
||||
for i := range c.s {
|
||||
c.s[i].m = make(map[string]int64)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ShardedCounter) shardFor(key string) *shard {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(key))
|
||||
return &c.s[h.Sum32()%shards]
|
||||
}
|
||||
|
||||
func (c *ShardedCounter) Inc(key string) {
|
||||
s := c.shardFor(key)
|
||||
s.mu.Lock()
|
||||
s.m[key]++
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *ShardedCounter) Get(key string) int64 {
|
||||
s := c.shardFor(key)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.m[key]
|
||||
}
|
||||
|
||||
func (c *ShardedCounter) Snapshot() map[string]int64 {
|
||||
out := make(map[string]int64)
|
||||
for i := range c.s {
|
||||
c.s[i].mu.Lock()
|
||||
for k, v := range c.s[i].m {
|
||||
out[k] = v
|
||||
}
|
||||
c.s[i].mu.Unlock()
|
||||
}
|
||||
return out
|
||||
}
|
||||
```
|
||||
</details>
|
||||
Reference in New Issue
Block a user