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
480 lines
8.2 KiB
Markdown
480 lines
8.2 KiB
Markdown
# 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>
|