Methods on Generic Types — Tasks¶
Exercise structure¶
- 🟢 Easy — for beginners
- 🟡 Medium — middle level
- 🔴 Hard — senior level
- 🟣 Expert — professional level
A solution for each exercise is provided at the end. Each task practises the mechanics of generic-type methods — receiver syntax, pointer vs value receivers, embedding, fluent chaining, and the free-function workaround.
Easy 🟢¶
Task 1 — Pair with Swap¶
Implement Pair[K, V any] with fields Key K, Value V and a method Swap() Pair[V, K].
Task 2 — Optional[T] basics¶
Implement Optional[T any] with constructors Some[T any](v T) Optional[T] and None[T any]() Optional[T], plus methods Get() (T, bool), IsSet() bool.
Task 3 — Counter for numeric T¶
Implement Counter[T ~int | ~int64] with Add(d T), Sub(d T), Value() T, Reset(). Use pointer receivers where appropriate.
Task 4 — Stack[T] complete¶
Write Stack[T any] with Push(v T), Pop() (T, bool), Peek() (T, bool), Len() int. Choose receivers carefully.
Task 5 — Box[T] with String¶
Add String() string to Box[T any] so fmt.Println calls it. Verify Box[int]{v: 7} satisfies fmt.Stringer.
Medium 🟡¶
Task 6 — Fluent Builder¶
Implement Builder[T any] with chained methods Add(v T) *Builder[T], Label(s string) *Builder[T], Build() ([]T, string). Each call returns *Builder[T].
Task 7 — Optional with chained methods¶
Add chained methods to Optional[T]: Or(other Optional[T]) Optional[T], Default(v T) T. Why is Map not possible as a method?
Task 8 — Cache[K, V] with TTL¶
Implement Cache[K comparable, V any] with Set(k K, v V), Get(k K) (V, bool), Delete(k K). Add Len() int.
Task 9 — Embed a generic type¶
Make LabeledList[T any] embed *List[T] (use the linked list from a previous topic). Add Label string and String() string. Verify methods are promoted.
Task 10 — Method shadowing¶
Embed *Box[T] in LoudBox[T any] and override String() to add an exclamation mark. Show how to call the inner method explicitly.
Task 11 — Interface satisfaction per instantiation¶
Define type IntPusher interface { Push(int) }. Verify that *Stack[int] satisfies it but *Stack[string] does not.
Task 12 — Free function for Map¶
Add a free function Map[T, U any](s *Stack[T], f func(T) U) *Stack[U]. Why can this not be a method?
Task 13 — Constraint-tight wrapper¶
Define Bag[T any] with Add(v T). Define ComparableBag[T comparable] that embeds *Bag[T] and adds Distinct() []T. Use both in main.
Task 14 — Method values¶
Given s := &Stack[int]{}, store s.Push in a variable push and call it three times. What is the type of push?
Hard 🔴¶
Task 15 — Generic LRU cache with methods¶
Implement LRU[K comparable, V any] with Get, Put, Len, Cap. Use a doubly-linked list internally. Add an iterator method ForEach(func(K, V)).
Task 16 — Atomic[T any]¶
Wrap sync/atomic.Value in Atomic[T any] with methods Load() (T, bool), Store(v T). Why does Load need (T, bool) rather than just T?
Task 17 — Pub/sub Bus[T any]¶
Implement Bus[T any] with Subscribe(func(T)) Cancel, Publish(v T). The Cancel is a func() returned to the subscriber. Use a mutex for thread safety.
Task 18 — Multi-embedding ambiguity¶
Create types A[T any] and B[T any] each with method Name() string. Embed both in C[T any]. Demonstrate the ambiguity error and resolve it by adding a method on C.
Task 19 — Method expression¶
Write a function ApplyTo[T any](f func(*Stack[T], T), s *Stack[T], v T) that calls f(s, v). Pass (*Stack[int]).Push as f.
Expert 🟣¶
Task 20 — Nested generic types¶
Implement Tree[T cmp.Ordered] with nodes node[T cmp.Ordered]. Methods on Tree[T]: Insert(v T), Contains(v T) bool, InOrder() []T. Note how nested generic types must repeat the constraint.
Task 21 — Free-function Map vs method¶
Implement Map[T, U any](b Box[T], f func(T) U) Box[U] as a free function. Then attempt to write the same as a method to demonstrate the compile error.
Task 22 — Generic method captured in closure¶
Write a function that takes *Counter[int] and returns a closure that increments it. Show how the receiver is captured.
Solutions¶
Solution 1¶
type Pair[K, V any] struct {
Key K
Value V
}
func (p Pair[K, V]) Swap() Pair[V, K] {
return Pair[V, K]{Key: p.Value, Value: p.Key}
}
Solution 2¶
type Optional[T any] struct {
value T
set bool
}
func Some[T any](v T) Optional[T] { return Optional[T]{value: v, set: true} }
func None[T any]() Optional[T] { return Optional[T]{} }
func (o Optional[T]) Get() (T, bool) { return o.value, o.set }
func (o Optional[T]) IsSet() bool { return o.set }
Solution 3¶
type Counter[T ~int | ~int64] struct {
n T
}
func (c *Counter[T]) Add(d T) { c.n += d }
func (c *Counter[T]) Sub(d T) { c.n -= d }
func (c *Counter[T]) Reset() { var zero T; c.n = zero }
func (c Counter[T]) Value() T { return c.n }
Solution 4¶
type Stack[T any] struct{ data []T }
func (s *Stack[T]) Push(v T) { s.data = append(s.data, v) }
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.data) == 0 { return zero, false }
n := len(s.data) - 1
v := s.data[n]
s.data = s.data[:n]
return v, true
}
func (s Stack[T]) Peek() (T, bool) {
var zero T
if len(s.data) == 0 { return zero, false }
return s.data[len(s.data)-1], true
}
func (s Stack[T]) Len() int { return len(s.data) }
Solution 5¶
type Box[T any] struct{ v T }
func (b Box[T]) String() string { return fmt.Sprintf("Box(%v)", b.v) }
var s fmt.Stringer = Box[int]{v: 7}
fmt.Println(s) // Box(7)
Solution 6¶
type Builder[T any] struct {
items []T
label string
}
func (b *Builder[T]) Add(v T) *Builder[T] { b.items = append(b.items, v); return b }
func (b *Builder[T]) Label(s string) *Builder[T] { b.label = s; return b }
func (b *Builder[T]) Build() ([]T, string) { return b.items, b.label }
// Usage:
items, label := (&Builder[int]{}).Add(1).Add(2).Label("nums").Build()
Solution 7¶
func (o Optional[T]) Or(other Optional[T]) Optional[T] {
if o.set { return o }
return other
}
func (o Optional[T]) Default(v T) T {
if o.set { return o.value }
return v
}
Map would need a new type parameter U (the result type), which methods cannot introduce. Workaround: free function. Solution 8¶
type Cache[K comparable, V any] struct {
m map[K]V
}
func NewCache[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{m: map[K]V{}}
}
func (c *Cache[K, V]) Set(k K, v V) { c.m[k] = v }
func (c *Cache[K, V]) Get(k K) (V, bool) { v, ok := c.m[k]; return v, ok }
func (c *Cache[K, V]) Delete(k K) { delete(c.m, k) }
func (c *Cache[K, V]) Len() int { return len(c.m) }
Solution 9¶
type LabeledList[T any] struct {
*List[T]
Label string
}
func (l LabeledList[T]) String() string {
return fmt.Sprintf("%s(len=%d)", l.Label, l.Len())
}
*List[T] like PushFront, PushBack, Len are promoted. Solution 10¶
type LoudBox[T any] struct{ *Box[T] }
func (l LoudBox[T]) String() string {
return l.Box.String() + "!"
}
// inner: l.Box.String()
// outer: l.String()
Solution 11¶
type IntPusher interface { Push(int) }
var p1 IntPusher = &Stack[int]{}
// var p2 IntPusher = &Stack[string]{} // ❌ — Push(string) does not match Push(int)
Solution 12¶
func Map[T, U any](s *Stack[T], f func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.data { out.Push(f(v)) }
return out
}
U; only the receiver type's parameters are visible. Solution 13¶
type Bag[T any] struct{ items []T }
func (b *Bag[T]) Add(v T) { b.items = append(b.items, v) }
type ComparableBag[T comparable] struct{ *Bag[T] }
func (b ComparableBag[T]) Distinct() []T {
seen := map[T]struct{}{}
out := make([]T, 0, len(b.items))
for _, v := range b.items {
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
return out
}
Solution 14¶
The method value capturess and resolves T to int. Solution 15¶
type lruEntry[K comparable, V any] struct {
k K; v V
prev, next *lruEntry[K, V]
}
type LRU[K comparable, V any] struct {
cap int
m map[K]*lruEntry[K, V]
head, tail *lruEntry[K, V]
}
func NewLRU[K comparable, V any](cap int) *LRU[K, V] {
return &LRU[K, V]{cap: cap, m: map[K]*lruEntry[K, V]{}}
}
func (l *LRU[K, V]) Cap() int { return l.cap }
func (l *LRU[K, V]) Len() int { return len(l.m) }
// Get/Put/ForEach: standard LRU implementation
Solution 16¶
import "sync/atomic"
type Atomic[T any] struct{ v atomic.Value }
func (a *Atomic[T]) Store(v T) { a.v.Store(v) }
func (a *Atomic[T]) Load() (T, bool) {
v := a.v.Load()
if v == nil { var zero T; return zero, false }
return v.(T), true
}
Load needs the bool because atomic.Value returns nil for unset values; we cannot represent "no value" inside T alone. Solution 17¶
type Bus[T any] struct {
mu sync.Mutex
subs []func(T)
}
type Cancel func()
func (b *Bus[T]) Subscribe(f func(T)) Cancel {
b.mu.Lock()
defer b.mu.Unlock()
idx := len(b.subs)
b.subs = append(b.subs, f)
return func() {
b.mu.Lock()
defer b.mu.Unlock()
b.subs[idx] = nil
}
}
func (b *Bus[T]) Publish(v T) {
b.mu.Lock()
subs := append([]func(T){}, b.subs...)
b.mu.Unlock()
for _, f := range subs {
if f != nil { f(v) }
}
}
Solution 18¶
type A[T any] struct{}
func (A[T]) Name() string { return "A" }
type B[T any] struct{}
func (B[T]) Name() string { return "B" }
type C[T any] struct {
A[T]
B[T]
}
func (c C[T]) Name() string { return "C(" + c.A.Name() + "+" + c.B.Name() + ")" }
// Without C.Name: c.Name() is ambiguous → compile error.
Solution 19¶
func ApplyTo[T any](f func(*Stack[T], T), s *Stack[T], v T) { f(s, v) }
s := &Stack[int]{}
ApplyTo((*Stack[int]).Push, s, 42)
(*Stack[int]).Push has type func(*Stack[int], int), matching f. Solution 20¶
type node[T cmp.Ordered] struct {
v T
left, right *node[T]
}
type Tree[T cmp.Ordered] struct{ root *node[T] }
func (t *Tree[T]) Insert(v T) { t.root = insert(t.root, v) }
func insert[T cmp.Ordered](n *node[T], v T) *node[T] {
if n == nil { return &node[T]{v: v} }
switch {
case v < n.v: n.left = insert(n.left, v)
case v > n.v: n.right = insert(n.right, v)
}
return n
}
func (t *Tree[T]) Contains(v T) bool { return contains(t.root, v) }
func contains[T cmp.Ordered](n *node[T], v T) bool {
for n != nil {
switch {
case v < n.v: n = n.left
case v > n.v: n = n.right
default: return true
}
}
return false
}
func (t *Tree[T]) InOrder() []T {
out := []T{}
var walk func(*node[T])
walk = func(n *node[T]) {
if n == nil { return }
walk(n.left); out = append(out, n.v); walk(n.right)
}
walk(t.root)
return out
}
node[T] repeats the constraint cmp.Ordered. Solution 21¶
// Free function — works
func Map[T, U any](b Box[T], f func(T) U) Box[U] {
return Box[U]{v: f(b.v)}
}
// Method version — does NOT compile:
// func (b Box[T]) Map[U any](f func(T) U) Box[U] { ❌ }
Solution 22¶
func makeIncrementer(c *Counter[int]) func() {
return func() { c.Add(1) }
}
c := &Counter[int]{}
inc := makeIncrementer(c)
inc(); inc(); inc()
fmt.Println(c.Value()) // 3
c (the receiver pointer), not the method itself. Equivalent to inc := func() { c.Add(1) }. Final notes¶
Patterns reinforced by these tasks:
- Receiver always repeats
[T](Tasks 1-5). - Pointer receiver for mutation (Tasks 3, 4, 8).
- Fluent chaining returns the receiver type (Task 6).
- No method-level type parameters — use a free function (Tasks 12, 21).
- Embedding promotes methods after substitution (Tasks 9, 10).
- Multi-embedding causes ambiguity unless resolved (Task 18).
- Method values resolve the type parameter at binding time (Task 14).
- Method expressions need explicit instantiation (Task 19).
Senior advice: when designing a generic type, list its methods on paper first. If any method "wants" to introduce a new type parameter, plan a free function instead. The constraint-once rule shapes the entire API.