Generic Types & Interfaces — Tasks¶
This file contains 20+ exercises, ordered easy → hard. Each task has a goal, a starter signature where helpful, and acceptance criteria. Solutions are not included — try them first, then check against the standard library or your peers.
Easy¶
Task 1 — Pair[K, V]¶
Define a generic struct with Key and Value fields. Add constructors NewPair[K, V] and a method String() string that requires K to be a fmt.Stringer.
Hint: you may need two types: one for general K, V, one for stringable K, V. Or take a keyToString function in the constructor.
Acceptance: compiles for Pair[string, int].
Task 2 — Set[T]¶
Implement a generic set. Methods:
Add(v T)Has(v T) boolRemove(v T)Len() intForEach(fn func(T))
Use T comparable.
Acceptance: tests with Set[int] and Set[string] pass.
Task 3 — Stack[T] revisited¶
Re-implement the canonical Stack[T] from junior.md. Add Peek() (T, bool) and Clear(). Cover with table-driven tests.
Task 4 — Queue[T]¶
A FIFO queue with Enqueue, Dequeue, Len. Use a slice underneath; periodically reslice to avoid unbounded growth.
Task 5 — Optional[T]¶
type Optional[T any] struct { /* ... */ }
func Some[T any](v T) Optional[T] { ... }
func None[T any]() Optional[T] { ... }
func (o Optional[T]) Get() (T, bool) { ... }
func (o Optional[T]) OrElse(def T) T { ... }
Add a function Map[T, U any](o Optional[T], fn func(T) U) Optional[U]. (Top-level — not a method, because U is new.)
Medium¶
Task 6 — Result[T]¶
A value-or-error envelope with Ok, Err, IsOk, Unwrap. Add Then[T, U any](r Result[T], fn func(T) Result[U]) Result[U] as a top-level function (monadic chain).
Task 7 — OrderedMap[K, V]¶
Like a regular map but preserves insertion order. Methods:
Set(k K, v V)Get(k K) (V, bool)Delete(k K)Keys() []KValues() []VForEach(fn func(K, V))
Hint: keep a slice of keys alongside a map[K]V.
Task 8 — RingBuffer[T]¶
Fixed-capacity ring buffer:
func NewRingBuffer[T any](cap int) *RingBuffer[T]
func (r *RingBuffer[T]) Push(v T) (overwritten bool, oldValue T)
func (r *RingBuffer[T]) Pop() (T, bool)
func (r *RingBuffer[T]) Len() int
func (r *RingBuffer[T]) Cap() int
Push on a full buffer overwrites the oldest element. Return whether overwriting happened and the displaced value.
Task 9 — Iterator[T] interface + helpers¶
Define:
Implement:
SliceIter[T any]([]T) Iterator[T]MapIter[T, U any](Iterator[T], func(T) U) Iterator[U]FilterIter[T any](Iterator[T], func(T) bool) Iterator[T]Take[T any](Iterator[T], n int) Iterator[T]Collect[T any](Iterator[T]) []T
These are top-level functions (not methods on Iterator[T]).
Task 10 — LinkedList[T]¶
Singly-linked list with Prepend, Append, ForEach, Reverse, Len. Internal node[T] should be unexported.
Task 11 — BinaryTree[T]¶
A binary search tree. Insertion takes a less func(a, b T) bool (passed in; do not constrain T).
Insert(v T)Contains(v T) boolInOrder() []TPreOrder() []T
Task 12 — Comparator[T] + sorting¶
type Comparator[T any] interface { Compare(a, b T) int }
func SortWith[T any](xs []T, c Comparator[T])
Implement a basic insertion sort using Comparator[T]. Then write a LessFuncComparator[T] that wraps a func(a, b T) bool into a Comparator[T].
Task 13 — Generic graph¶
type Graph[T comparable] struct { /* ... */ }
func (g *Graph[T]) AddNode(v T)
func (g *Graph[T]) AddEdge(from, to T)
func (g *Graph[T]) Neighbors(v T) []T
func (g *Graph[T]) BFS(start T, visit func(T))
func (g *Graph[T]) DFS(start T, visit func(T))
Acceptance: tested with Graph[int] and Graph[string].
Task 14 — Generic cache with TTL¶
Implement Cache[K comparable, V any] with TTL eviction. Provide:
Set(k K, v V)Get(k K) (V, bool)Delete(k K)StartCleaner(interval time.Duration, stop <-chan struct{})
Make it concurrency-safe via sync.RWMutex.
Hard¶
Task 15 — Generic LRU cache¶
Implement LRU[K comparable, V any] with bounded capacity. Eviction by least-recently-used. Internally combine a map[K]*entry[K, V] and a doubly-linked list of entry[K, V].
Set(k K, v V)— evict LRU if over capacityGet(k K) (V, bool)— bumps the entry to MRULen() intPeek(k K) (V, bool)— does not bump
Task 16 — Generic event bus with cancel tokens¶
type EventBus[E any] struct { /* ... */ }
func (b *EventBus[E]) Subscribe(h func(E)) (unsub func())
func (b *EventBus[E]) Publish(e E)
Multiple subscribers; Publish calls them all; unsub removes the handler. Make it safe under concurrent Subscribe/Publish.
Task 17 — Generic worker pool¶
type Pool[In, Out any] struct { /* ... */ }
func NewPool[In, Out any](workers int, fn func(context.Context, In) (Out, error)) *Pool[In, Out]
func (p *Pool[In, Out]) Submit(in In) <-chan Result[Out]
func (p *Pool[In, Out]) Close()
Result on a channel; cancel via context.
Task 18 — Generic pipeline composer¶
Write three top-level helpers:
Stage[In, Out any](ctx, in <-chan In, fn func(In) (Out, error)) (<-chan Out, <-chan error)Source[T any](items []T) <-chan TSink[T any](in <-chan T, fn func(T))
Compose three stages on int → string → []byte and verify end-to-end behavior.
Task 19 — KeyValueStore[K, V] interface + 3 implementations¶
Define the interface:
Implement three storage backends:
- In-memory
map[K]Vwith a mutex. - File-backed JSON store (one file per key).
- Wrapper that adds metrics (count of Gets/Sets) without changing semantics.
Switch implementations via a constructor option.
Task 20 — Generic state machine¶
type StateMachine[S comparable, E comparable] struct { /* ... */ }
func NewStateMachine[S comparable, E comparable](initial S) *StateMachine[S, E]
func (m *StateMachine[S, E]) AddTransition(from S, on E, to S)
func (m *StateMachine[S, E]) Fire(e E) (S, bool)
func (m *StateMachine[S, E]) State() S
Tested with a traffic-light FSM (type Light int; type Tick struct{}).
Task 21 — Concurrent-safe sharded map¶
type ShardedMap[K comparable, V any] struct { /* ... */ }
func NewShardedMap[K comparable, V any](shards int, hash func(K) uint64) *ShardedMap[K, V]
func (m *ShardedMap[K, V]) Get(k K) (V, bool)
func (m *ShardedMap[K, V]) Set(k K, v V)
Each shard has its own mutex; routing via hash(k) % shards.
Bonus: add RangeShard(idx int, fn func(K, V) bool) that holds only one shard's lock at a time.
Task 22 — Generic Bloom filter¶
type BloomFilter[T any] struct { /* ... */ }
func NewBloomFilter[T any](size int, k int, hash func(T, int) uint64) *BloomFilter[T]
func (b *BloomFilter[T]) Add(v T)
func (b *BloomFilter[T]) MaybeContains(v T) bool
The hash function takes the value and an index 0..k-1. Test with strings and ints.
Task 23 — Generic merge sort¶
Pure functional style — return a new slice. Make sure it's stable.
Task 24 — Generic priority queue¶
type PriorityQueue[T any] struct { /* ... */ }
func NewPriorityQueue[T any](less func(a, b T) bool) *PriorityQueue[T]
func (pq *PriorityQueue[T]) Push(v T)
func (pq *PriorityQueue[T]) Pop() (T, bool)
func (pq *PriorityQueue[T]) Peek() (T, bool)
func (pq *PriorityQueue[T]) Len() int
Use a binary heap. Bonus: build it on top of container/heap by providing the right adapter (this requires a non-generic struct that wraps a []T).
Task 25 — Generic transactional store¶
type TxStore[K comparable, V any] struct { /* ... */ }
func (s *TxStore[K, V]) Begin() *Tx[K, V]
type Tx[K comparable, V any] struct { /* ... */ }
func (t *Tx[K, V]) Get(k K) (V, bool)
func (t *Tx[K, V]) Set(k K, v V)
func (t *Tx[K, V]) Commit() error
func (t *Tx[K, V]) Rollback()
A simple in-memory MVCC: reads see a snapshot; writes happen in the transaction; commit applies them atomically; rollback discards.
Extra credit¶
- E1: Try to write
func (s *Stack[T]) MapTo[U any](fn func(T) U) *Stack[U]and observe the compiler error. Translate the message in your own words. - E2: Write a
Cache[K, V]whose values must satisfyfmt.Stringer. Compose a constraint that requires bothK comparableandV fmt.Stringer. - E3: Use the
cmp.Orderedconstraint (Go 1.21+) to write a genericMinMax[T cmp.Ordered](xs []T) (min, max T). - E4: Compare a generic
Sum[T Numeric](xs []T) Tagainst afunc Sum(xs []int64) int64using benchmarks. - E5: Convert a real package in your work codebase that uses
interface{}containers to generic equivalents. Measure the impact (binary size, benchmark, compile time).
Acceptance test idea — generic test helper¶
Use a helper to test multiple Ts with one body:
func testStack[T comparable](t *testing.T, items []T) {
s := NewStack[T]()
for _, v := range items { s.Push(v) }
for i := len(items) - 1; i >= 0; i-- {
v, ok := s.Pop()
if !ok || v != items[i] { t.Fatalf("pop wrong") }
}
}
func TestStackInt(t *testing.T) { testStack(t, []int{1, 2, 3}) }
func TestStackString(t *testing.T) { testStack(t, []string{"a", "b"}) }
This pattern is itself a great practice exercise — the helper is generic, the test functions are not.
End of tasks.md.