Files
learning-go-for-sre-interview/SOLUTIONS.md
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

8.2 KiB

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.

01-url-health-checker
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
}
02-worker-pool
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
}
03-rate-limiter
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
}
04-log-topn
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
}
05-lru-cache
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()
}
06-retry-backoff
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)
}
07-fan-in
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
}
08-graceful-shutdown
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
}
09-safe-counter
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
}