Generic Pitfalls — Find the Bug¶
How to use¶
Each problem shows a code snippet. Read it carefully and answer: 1. What is the bug? 2. How would you fix it? 3. Is it a pitfall (compiles but misbehaves) or a hard error (refuses to compile)?
Solutions are at the end. Most of these are realistic — patterns observed in real codebases.
Bug 1 — T{} composite literal¶
Hint: What kinds of types support composite literals?
Bug 2 — nil check for T any¶
Hint: What does the constraint any permit?
Bug 3 — Type switch on T directly¶
func Describe[T any](v T) string {
switch v.(type) {
case int: return "int"
case string: return "string"
}
return "?"
}
Hint: Type assertions require interface types.
Bug 4 — comparable instead of cmp.Ordered¶
Hint: What operators does comparable allow?
Bug 5 — typed-nil interface¶
Hint: What happens when you box a nil pointer into any?
Bug 6 — Useless T¶
Hint: Where is T used?
Bug 7 — empty constraint type set¶
type Strange interface {
~int
~string
}
func F[T Strange](v T) T { return v }
F(1) // ?
F("hello") // ?
Hint: Set intersection.
Bug 8 — pointer/value method-set¶
type Greeter interface { Greet() }
type Bot struct{ name string }
func (b *Bot) Greet() { fmt.Println("hi from", b.name) }
func RunGreeter[T Greeter](g T) { g.Greet() }
RunGreeter(Bot{name: "A"})
Hint: Whose method set contains Greet?
Bug 9 — Constraint-operation mismatch¶
Hint: What does comparable allow?
Bug 10 — IsZero for slice-typed T¶
Hint: Are slices comparable?
Bug 11 — runtime panic from relaxed comparable¶
func Eq[T comparable](a, b T) bool { return a == b }
var a, b any = []int{1, 2}, []int{1, 2}
fmt.Println(Eq(a, b))
Hint: Compiles in 1.20+. What happens when run?
Bug 12 — inference fails with function-typed parameter¶
func Apply[T, U any](f func(T) U) U {
var t T
return f(t)
}
r := Apply(func(int) string { return "" })
Hint: Where do T and U get pinned?
Bug 13 — reflecting nil interface¶
import "reflect"
func TypeName[T any](v T) string {
return reflect.TypeOf(v).Name()
}
var e error
fmt.Println(TypeName(e))
Hint: What does reflect.TypeOf return for nil interface?
Bug 14 — polymorphism by type switch¶
func Process[T any](v T) {
switch x := any(v).(type) {
case Dog: x.Bark()
case Cat: x.Meow()
}
}
Process(Fish{})
Hint: What happens for unhandled types? Is this really generic?
Bug 15 — Optional[T] everywhere¶
type Optional[T any] struct {
v T
has bool
}
func Find[T any](s []T, p func(T) bool) Optional[T] {
for _, v := range s {
if p(v) { return Optional[T]{v, true} }
}
return Optional[T]{}
}
Hint: Compare with idiomatic Go.
Solutions¶
Bug 1 — fix¶
Pitfall: hard error. T{} is a composite literal restricted to specific underlying kinds.
Bug 2 — fix¶
Pitfall: hard error. == requires comparable, and nil requires nilable.
Bug 3 — fix¶
Pitfall: hard error. Convert through any first:
T and consider using an interface instead. Bug 4 — fix¶
Pitfall: hard error. comparable does not allow <. Use cmp.Ordered:
Bug 5 — fix¶
Pitfall: silent wrong answer. IsNil(p) returns false because any(p) holds (*int, nil). Use reflection or restructure the API:
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:
return rv.IsNil()
}
return false
}
Bug 6 — fix¶
Pitfall: useless type parameter. Remove T:
Bug 7 — fix¶
Pitfall: empty type set. No type can satisfy. Both calls fail to compile. Use a union:
Bug 8 — fix¶
Pitfall: hard error. Greet is on *Bot, not Bot. Either change to value receiver or call with &Bot{...}:
Bug 9 — fix¶
Pitfall: hard error. comparable does not allow +. Use a numeric constraint:
Bug 10 — fix¶
Pitfall: hard error. []int is not comparable. Provide a slice helper:
Bug 11 — fix¶
Pitfall: silent runtime panic. == on two []int panics. The compiler allowed because of 1.20 relaxation. Defensive code: never pass slices/maps through comparable generics.
Bug 12 — fix¶
Pitfall: hard error or runtime weirdness. Pre-1.21 inference often fails. Specify explicitly:
Bug 13 — fix¶
Pitfall: panic. reflect.TypeOf(nil interface) returns nil; .Name() panics. Guard:
Bug 14 — fix¶
Pitfall: silent wrong behaviour. Process(Fish{}) matches no case and silently does nothing. This is interface dispatch in disguise. Use:
Bug 15 — fix¶
Pitfall: anti-pattern. Use Go's idiomatic (T, bool):
func Find[T any](s []T, p func(T) bool) (T, bool) {
for _, v := range s {
if p(v) { return v, true }
}
var zero T
return zero, false
}
Optional[T] adds a wrapper type that fights Go's idiom and adds an unwrap step at every boundary. Lessons¶
Patterns from these bugs:
- Composite literals never work for arbitrary
T any(Bug 1). niland==checks depend on the constraint (Bugs 2, 5, 9, 11).- Type switches require interfaces (Bug 3) — and even when allowed, often signal misuse (Bug 14).
- Constraints must match operations (Bugs 4, 9).
comparable≠cmp.Ordered. - Method sets differ between
Tand*T(Bug 8). reflect.TypeOfis sensitive to nil interfaces (Bug 13).- Useless type parameters add complexity without value (Bug 6).
- Empty type sets compile but accept nothing (Bug 7).
- Inference fails through function-typed arguments (Bug 12).
- Imported abstractions like
Optional[T]fight Go idioms (Bug 15).
A senior reviewer reads constraints and signatures with these patterns in mind. The questions are always the same: "Does the body's operations match the constraint? Is the type parameter doing useful work? Will inference work at the call site?" Mismatches between any of these are the category of generic bugs.