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.
In this topic