Generic Limitations — 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 asks you to identify a limit and write the canonical workaround.
Easy 🟢¶
Task 1 — Lift a method to a free function¶
You wrote:
type Box[T any] struct{ V T }
func (b Box[T]) Map[U any](f func(T) U) Box[U] { return Box[U]{V: f(b.V)} }
MapBox. Task 2 — Type-switch via any¶
The body
func Describe[T any](v T) string {
switch v.(type) { case int: return "int"; case string: return "str" }
return "?"
}
Task 3 — Convert []Cat to []Animal¶
Given:
type Animal interface{ Name() string }
type Cat struct{}
func (Cat) Name() string { return "cat" }
cats := []Cat{{}, {}}
[]Animal from cats. Task 4 — len on a generic¶
Why does this fail? Add a constraint that makes it compile. Task 5 — make on a generic¶
Why does this fail? Rewrite to allocate an empty slice for any element type E. Medium 🟡¶
Task 6 — Polymorphism in disguise¶
Refactor the following to an interface, removing the runtime type-switch:
func Run[T any](v T) {
switch x := any(v).(type) {
case *Order: x.Process()
case *Refund: x.Process()
}
}
Task 7 — Free Filter over a Stack[T]¶
Given type Stack[T any] struct{ data []T }, write a free function FilterStack[T any](s *Stack[T], keep func(T) bool) *Stack[T].
Task 8 — MapPair¶
Given type Pair[A, B any] struct{ First A; Second B }, write a free MapPair[A, B, C, D any](p Pair[A, B], f func(A) C, g func(B) D) Pair[C, D].
Task 9 — Generic Coalesce without specialization¶
The hot path is int. Write Coalesce[T comparable] for the generic case AND CoalesceInt for the specialized hot type. Discuss why you cannot have one body that auto-specializes.
Task 10 — Stack of pointers vs values¶
Why is Stack[*int] a different type from Stack[int]? Demonstrate with a compile error and explain.
Task 11 — Interface with method type parameter¶
Why does this fail? Provide a free-function alternative.Task 12 — Constraint with ~¶
Write a constraint Number allowing int, float64, and any defined type whose underlying type is int or float64. Then write Sum[T Number](s []T) T.
Task 13 — Slice constraint¶
Write func Reverse[E any, S ~[]E](s S) S that reverses a slice of any underlying-slice type. Why does the constraint need both E and S?
Task 14 — Type-switch at the boundary¶
Write a logging function Log[T any](v T) that handles string specially, falls back to fmt.Sprintf("%v", v) for everything else. Discuss the cost of the workaround.
Hard 🔴¶
Task 15 — HKT-free Map per container¶
You want a generic Map that works on []T, map[K]V, and chan T. Why can't one signature do it? Provide three free functions.
Task 16 — Generic-type-alias workaround pre-1.24¶
Pretend you are on Go 1.22. You want type Vec[T any] = []T. The compiler refuses. Provide the type-definition workaround and discuss the differences in identity and method set.
Task 17 — Free Reduce over a generic type¶
Given type Tree[T any] struct{ ... }, write a free ReduceTree[T, R any](t *Tree[T], init R, f func(R, T) R) R. Explain why this cannot be a method.
Task 18 — Polymorphism vs parameterism diagnostic¶
Given:
func Handle[T any](v T) error {
switch x := any(v).(type) {
case *Email: return x.Send()
case *SMS: return x.Send()
}
return errors.New("unknown")
}
Task 19 — Specialization without language support¶
You have Hash[T any](v T) uint64 and want int to take a fast path. Write the hot-path branch using any(v).(type) and benchmark vs a hand-specialized HashInt. Discuss when the branch overhead beats the speedup.
Expert 🟣¶
Task 20 — Layered library with reflection cache¶
Design a Decode[T any](data []byte) (T, error) that uses cached reflection internally. Show how the public API stays clean while the internal layer hides the reflection cost.
Task 21 — Interface embedding with type-parameter clash¶
Given type Container[T any] struct{ Inner Box[T] }, write methods on Container that delegate to Box. Note the verbosity of repeating [T] and discuss whether free functions are cleaner.
Task 22 — Audit a generic API for limits¶
Take a small generic library (your own or samber/lo) and identify three places where a limit shaped the API. Document each with the proposal number or spec reference.
Solutions¶
Solution 1¶
type Box[T any] struct{ V T }
func MapBox[T, U any](b Box[T], f func(T) U) Box[U] {
return Box[U]{V: f(b.V)}
}
Solution 2¶
func Describe[T any](v T) string {
switch any(v).(type) {
case int: return "int"
case string: return "str"
}
return "?"
}
any(v) produces one. Solution 3¶
Slices are invariant. Element-by-element copy is the canonical workaround.Solution 4¶
len is not defined for arbitrary T. Add a constraint:
Solution 5¶
make(T) requires T to be a slice/map/chan kind. The compiler cannot guarantee that for T any. Rewrite:
Solution 6¶
Polymorphism (different behaviour per type) belongs to interfaces, not generics.Solution 7¶
func FilterStack[T any](s *Stack[T], keep func(T) bool) *Stack[T] {
out := &Stack[T]{}
for _, v := range s.data { if keep(v) { out.data = append(out.data, v) } }
return out
}
Solution 8¶
func MapPair[A, B, C, D any](p Pair[A, B], f func(A) C, g func(B) D) Pair[C, D] {
return Pair[C, D]{First: f(p.First), Second: g(p.Second)}
}
Solution 9¶
func Coalesce[T comparable](vals ...T) T {
var zero T
for _, v := range vals { if v != zero { return v } }
return zero
}
func CoalesceInt(vals ...int) int {
for _, v := range vals { if v != 0 { return v } }
return 0
}
CoalesceInt may inline more aggressively. PGO may close part of the gap automatically in 1.21+. Solution 10¶
Each instantiation is its own type. There is no implicit conversion across element types.Solution 11¶
// Failed:
// type Mapper interface { Map[U any](f func(int) U) Mapper }
// Free function workaround:
type IntSlice []int
func MapMapper[U any](m IntSlice, f func(int) U) []U {
out := make([]U, len(m))
for i, v := range m { out[i] = f(v) }
return out
}
Solution 12¶
type Number interface { ~int | ~float64 }
func Sum[T Number](s []T) T {
var total T
for _, v := range s { total += v }
return total
}
Solution 13¶
func Reverse[E any, S ~[]E](s S) S {
out := make(S, len(s))
for i, v := range s { out[len(s)-1-i] = v }
return out
}
S ~[]E to preserve the original named slice type in the return; E is needed inside to talk about the element type. Solution 14¶
func Log[T any](v T) string {
switch x := any(v).(type) {
case string: return "str:" + x
default: return fmt.Sprintf("%v", v)
}
}
v into interface{}. For string (already pointer-shaped) the cost is small. For value types it may allocate. Solution 15¶
func MapSlice[T, U any](s []T, f func(T) U) []U { ... }
func MapMap[K comparable, V, U any](m map[K]V, f func(K, V) U) []U { ... }
func MapChan[T, U any](c <-chan T, f func(T) U) <-chan U { ... }
Map[F[_], T, U], but Go does not have them. Per-container free functions are the idiomatic workaround. Solution 16¶
// Pre-1.24: type definition, not alias
type Vec[T any] []T
// Vec[int] is a NEW named type, not the same as []int
var v Vec[int] = Vec[int]{1, 2, 3}
// var s []int = v // would compile (slice conversion is implicit only via copy)
[]int. Solution 17¶
type Tree[T any] struct{ /* ... */ }
func ReduceTree[T, R any](t *Tree[T], init R, f func(R, T) R) R {
/* in-order traversal accumulating init via f */
return init
}
(*Tree[T]).Reduce[R any] would need a method type parameter, which is forbidden. Solution 18¶
The limit being misused is "type-switch on T". Both *Email and *SMS have a Send() method — that is polymorphism. The right shape is an interface:
Solution 19¶
func Hash[T any](v T) uint64 {
if x, ok := any(v).(int); ok { return uint64(x) * 2654435761 }
return slowHash(v)
}
func HashInt(v int) uint64 { return uint64(v) * 2654435761 }
HashInt is fastest. The Hash[int] branch adds an any(v) boxing and a type-assertion cost. PGO in 1.21+ may collapse this in profiled binaries. Solution 20¶
type meta struct { /* cached field info */ }
var cache sync.Map // map[reflect.Type]*meta
func metaOf(t reflect.Type) *meta {
if m, ok := cache.Load(t); ok { return m.(*meta) }
m := buildMeta(t)
cache.Store(t, m)
return m
}
func Decode[T any](data []byte) (T, error) {
var t T
rv := reflect.ValueOf(&t).Elem()
m := metaOf(rv.Type())
if err := decodeWith(rv, m, data); err != nil {
return t, err
}
return t, nil
}
Solution 21¶
type Container[T any] struct{ Inner Box[T] }
func (c Container[T]) Get() T { return c.Inner.V }
func (c Container[T]) Map(f func(T) T) Container[T] {
return Container[T]{Inner: Box[T]{V: f(c.Inner.V)}}
}
[T]. For type-changing transforms, use a free function MapContainer[T, U any]. This is cleaner than trying to get a method type parameter. Solution 22¶
Sample audit findings (illustrative): 1. lo.Map is a top-level function, not a method on a slice — because methods cannot have new type parameters (proposal 47781). 2. lo.Reduce has separate signatures for slice and map — because Go has no HKT. 3. Many helpers take (item T, index int) callbacks — because Go has no covariance, the signatures are uniform across call sites. Each is a direct consequence of a documented limit.
Final notes¶
These tasks emphasize recognition: when you hit a compile error related to generics, the first question is which of the well-known limits you triggered. Once recognized, the workaround is mechanical. The code rarely changes shape much; only the function name moves from a method to a free function, or a type-switch gains an any(v) cast.
The deeper lesson is that limits push you toward better designs. Most of the time, fighting the limit produces worse code than accepting the workaround.