Generic Pitfalls — 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 spot a pitfall in given code and fix it — the focus is recognition and correction, not greenfield design.
Easy 🟢¶
Task 1 — Zero value of T¶
The following does not compile. Fix it.
Task 2 — Nil check on T¶
This refuses to compile. Why, and how do you fix it?
Task 3 — Type switch on T¶
Make this compile.
func Print[T any](v T) {
switch v.(type) {
case int:
fmt.Println("int")
case string:
fmt.Println("string")
}
}
Task 4 — Wrong constraint¶
This errors with "operator < not defined". Fix the constraint.
Task 5 — any vs interface{}¶
Refactor this snippet to use any consistently. Identify all the places that mix styles.
func Wrap(items []interface{}) []any {
out := make([]any, len(items))
for i, v := range items { out[i] = v }
return out
}
Medium 🟡¶
Task 6 — IsZero for slices¶
Why does this not compile? Provide a fix that works for slices.
Task 7 — Pointer vs value method set¶
This rejects User{} but accepts &User{}. Explain and provide both possible fixes.
type Nameable interface { Name() string }
type User struct{ name string }
func (u *User) Name() string { return u.name }
func PrintName[T Nameable](v T) { fmt.Println(v.Name()) }
PrintName(User{}) // ❌
PrintName(&User{}) // ✓
Task 8 — Inference failure¶
Make this compile without changing the function definition.
Task 9 — Useless type parameter¶
Identify why T is useless here. Refactor.
Task 10 — Empty constraint type set¶
What is wrong with this constraint? Fix it.
Task 11 — Typed-nil interface¶
Predict and fix.
Task 12 — Type-switch trap¶
Refactor to avoid type-switching on T.
func Encode[T any](v T) []byte {
switch x := any(v).(type) {
case string: return []byte(x)
case int: return []byte(strconv.Itoa(x))
}
panic("unsupported")
}
Task 13 — Constraint-operation mismatch¶
What does this body need that the constraint does not allow?
Task 14 — Comparable-relaxation gotcha¶
What might happen at runtime?
Hard 🔴¶
Task 15 — Lost inlining¶
Look at a generic function and predict whether it will inline. Run go build -gcflags=-m on:
func Find[T comparable](s []T, target T) int {
for i, v := range s {
if v == target { return i }
}
return -1
}
FindInt. Discuss what you see. Task 16 — Dictionary cost benchmark¶
Write a benchmark that compares:
Explain why or why not the generic is slower.Task 17 — Reflect inside generics¶
The following panics for some inputs. Fix.
import "reflect"
func TypeName[T any](v T) string {
return reflect.TypeOf(v).Name()
}
var p *int
fmt.Println(TypeName(p)) // ?
var e error
fmt.Println(TypeName(e)) // ?
Task 18 — Method-set asymmetry¶
Design a constraint that requires a Close() error method on the pointer type of T, allowing callers to pass a value T.
Task 19 — Generic god type¶
Refactor this into smaller pieces.
type Pipeline[T, U, V, W any] struct {
f func(T) U
g func(U) V
h func(V) W
}
func (p Pipeline[T, U, V, W]) Run(t T) W { return p.h(p.g(p.f(t))) }
Expert 🟣¶
Task 20 — Constraint audit¶
You inherit a package with 25 constraints, half unused. Write a script that lists each constraint and its callers, and propose a deletion plan.
Task 21 — Cross-package instantiation¶
Create three packages: genericpkg defining Find[T comparable], caller_a and caller_b each instantiating it with different types. Use go tool nm to inspect duplicate symbols. Discuss the build-cache implications.
Task 22 — Migration playbook¶
Take an interface{}-based Cache:
type Cache struct{ m map[string]interface{} }
func (c *Cache) Set(k string, v interface{}) { c.m[k] = v }
func (c *Cache) Get(k string) interface{} { return c.m[k] }
Solutions¶
Solution 1¶
T{} is a composite literal; only valid for struct/array/slice/map underlying types. var zero T is always valid. Solution 2¶
v == nil requires T to be nilable. Either tighten the constraint or rewrite as IsZero:
Solution 3¶
Convert through any:
func Print[T any](v T) {
switch any(v).(type) {
case int: fmt.Println("int")
case string: fmt.Println("string")
}
}
Solution 4¶
Use cmp.Ordered:
Solution 5¶
The function takes []interface{} and returns []any. Use any everywhere:
Solution 6¶
comparable excludes slices because they are not strictly comparable. Provide a slice-specialised helper:
reflect.DeepEqual if you really need a generic "is zero": Solution 7¶
The method Name belongs to *User's method set, not User's. Two fixes:
Option A: change to value receiver:
func (u User) Name() string { return u.name }
PrintName(User{}) // ✓
PrintName(&User{}) // ✓ (auto-addresses)
Option B: force pointer:
type Nameable[T any] interface {
*T
Name() string
}
func PrintName[T any, P Nameable[T]](p P) { fmt.Println(p.Name()) }
Solution 8¶
You cannot get Pair[int] because it is a partial instantiation that still needs B. Specify both:
x, y := Pair(1, "hi"). Solution 9¶
T is unused. Remove it.
Solution 10¶
The type set is empty (no type has both underlying-int and underlying-string). Use a union:
Solution 11¶
any(p) holds (*int, nil), which is not equal to bare nil. Compare differently:
func IsNil[T any](v T) bool {
rv := reflect.ValueOf(&v).Elem()
switch rv.Kind() {
case reflect.Pointer, reflect.Map, reflect.Slice, reflect.Chan, reflect.Func, reflect.Interface:
return rv.IsNil()
}
return false
}
Solution 12¶
Use a real interface:
Each type implementsEncode differently. Generics were not the right tool. Solution 13¶
The body uses +, but comparable only allows == and !=. Use a numeric constraint:
type Number interface { ~int | ~int64 | ~float32 | ~float64 }
func Sum[T Number](s []T) T {
var total T
for _, v := range s { total += v }
return total
}
Solution 14¶
At runtime, comparing two any values whose dynamic type is []int panics: "comparing uncomparable type []int". The compiler accepted because any satisfies comparable in 1.20+, but the runtime cannot do the comparison. Defensive: do not pass slices through comparable generic boundaries.
Solution 15¶
For T = int, the body is small and inlines. For diverse pointer-shaped types instantiated from many sites, the compiler may not inline. Use -gcflags="-m=2" and inspect.
Solution 16¶
Sketch:
func BenchmarkFindGeneric(b *testing.B) {
s := make([]int, 1000)
for i := 0; i < b.N; i++ { _ = FindS(s, 999) }
}
func BenchmarkFindHand(b *testing.B) {
s := make([]int, 1000)
for i := 0; i < b.N; i++ { _ = FindInt(s, 999) }
}
int. The dictionary cost is small here because == for int is inlined. Solution 17¶
Guard against nil:
func TypeName[T any](v T) string {
t := reflect.TypeOf(v)
if t == nil { return "<nil>" }
if t.Name() == "" { return t.String() }
return t.Name()
}
Solution 18¶
type Closeable[T any] interface {
*T
Close() error
}
func WithClose[T any, P Closeable[T]](p P) {
defer p.Close()
// ...
}
*T. Solution 19¶
Compose binary steps:
type Step[T, U any] func(T) U
func Chain[T, U, V any](a Step[T, U], b Step[U, V]) Step[T, V] {
return func(t T) V { return b(a(t)) }
}
Chain calls. Each call has manageable inference. Solution 20¶
Outline:
# List all interface declarations
grep -rE "^type [A-Z][a-zA-Z]+ interface" .
# For each, find usages
grep -r "\\[T <constraint>\\]" .
Solution 21¶
// genericpkg/find.go
package genericpkg
func Find[T comparable](s []T, t T) int { ... }
// caller_a/main.go
package main
import "genericpkg"
genericpkg.Find([]int{1, 2, 3}, 2)
// caller_b/main.go
package main
import "genericpkg"
genericpkg.Find([]string{"a", "b"}, "b")
go tool nm binary | grep Find — you should see genericpkg.Find[go.shape.int_0] and genericpkg.Find[go.shape.string_0] distinctly. Solution 22¶
Steps:
- Add new generic type
Cache[K comparable, V any]alongside. - Mark old methods
// Deprecated:. - Provide adapter:
- Migrate callers package by package.
- After a major version bump, delete
Cache.
Final notes¶
Each task above represents a real complaint that a junior or middle engineer has filed in the past. Solutions are short because the fix is usually a one-liner once you recognize the pattern. Recognition is the skill these tasks build.