Generic Types & Interfaces — Find the Bug¶
15+ buggy generic type/interface declarations. For each one: read the Code, try to spot the bug, then check the Hint, and finally compare with the Fix and Explanation.
Run each fix mentally with go vet ./... and go build ./... to confirm.
Bug 1 — receiver missing type parameters¶
Hint: the receiver *Stack is missing something.
Fix:
Explanation: methods on a generic type must declare the same type parameter list as the type itself, here [T]. Without it, the body cannot reference T, and the compiler emits undefined: T.
Bug 2 — method tries to add its own type parameter¶
type Stack[T any] struct { items []T }
func (s *Stack[T]) Map[U any](fn func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.items { out.items = append(out.items, fn(v)) }
return out
}
Hint: methods cannot do something that functions can.
Fix: turn the method into a top-level function:
func MapStack[T, U any](s *Stack[T], fn func(T) U) *Stack[U] {
out := &Stack[U]{}
for _, v := range s.items { out.items = append(out.items, fn(v)) }
return out
}
Explanation: Go forbids method-level type parameters. The error reads methods cannot have type parameters. Use a free function.
Bug 3 — using a constraint interface as a value type¶
Hint: what makes Number special?
Fix: use a regular type or generic function. For example:
Explanation: an interface containing a type set (int | float64) is constraint-only. Compile error: interface contains type constraints.
Bug 4 — missing constraint for map keys¶
Hint: not every T can be a map key.
Fix:
Explanation: only comparable types can be map keys. With any the compiler complains: invalid map key type T (missing comparable constraint).
Bug 5 — missing ~ in type set¶
type Numeric interface { int | float64 }
type Celsius float64
func Sum[T Numeric](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
var temps = []Celsius{20, 25, 30}
total := Sum(temps) // compile error
Hint: Celsius is a defined type based on float64.
Fix:
Explanation: without ~, only the exact types int and float64 are accepted. ~T widens the constraint to "any type with underlying type T".
Bug 6 — wrong instantiation count¶
type Pair[K, V any] struct { Key K; Value V }
func main() {
p := Pair[string]{Key: "x", Value: 1} // wrong
}
Hint: how many parameters does Pair have?
Fix:
Explanation: every type parameter must be instantiated. Compile error: wrong number of type arguments for type Pair.
Bug 7 — mixed receiver kinds¶
type Counter[T ~int | ~int64] struct { v T }
func (c Counter[T]) Inc() { c.v++ } // value receiver — bug
func (c *Counter[T]) Get() T { return c.v }
Hint: does Inc actually change anything?
Fix: use pointer receiver for Inc:
Explanation: Inc modifies c.v, but on a value receiver the change is on a copy. The original is unchanged. Mixed receiver kinds also cause method-set surprises (only *Counter[T] has both methods).
Bonus issue: the constraint here uses | directly without a constraint name; this is allowed inline but more readable as interface { ~int | ~int64 } named.
Bug 8 — interface mixing methods and type set, used as value¶
type Stringer = fmt.Stringer
type StringableNumber interface {
int | float64
Stringer
}
func main() {
var x StringableNumber = 1 // compile error
}
Hint: again, type set in an interface.
Fix: use it only as a constraint:
Explanation: any interface with a type set is constraint-only.
Bug 9 — comparable misuse¶
Hint: where can comparable actually appear?
Fix: comparable is a constraint, not a type. Use a generic type:
Explanation: error: cannot use comparable outside type constraints.
Bug 10 — var zero T forgotten¶
type Stack[T any] struct { items []T }
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 { return nil, false } // wrong
/* ... */
}
Hint: nil is not always a valid T.
Fix:
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 { return zero, false }
/* ... */
}
Explanation: for T = int, nil is invalid. The compiler error is cannot use nil as type T. Use var zero T.
Bug 11 — embedded generic interface forgets parameters¶
type Reader[T any] interface { Read() T }
type Closer interface { Close() error }
type RC interface {
Reader // missing parameter
Closer
}
Hint: Reader is generic.
Fix:
Explanation: every reference to a generic type must include the type arguments (or be itself generic). Otherwise: cannot use generic type Reader without instantiation.
Bug 12 — forgetting that two instantiations are different types¶
type Stack[T any] struct { items []T }
func main() {
var a Stack[int]
var b Stack[any]
a = b // compile error
}
Hint: does Stack[int] equal Stack[any]?
Fix: convert manually if you really need to:
b2 := Stack[int]{}
for _, v := range b.items {
if iv, ok := v.(int); ok {
b2.items = append(b2.items, iv)
}
}
a = b2
Explanation: Stack[int] and Stack[any] are different types. There is no implicit conversion.
Bug 13 — using == on a generic T any¶
func Find[T any](xs []T, v T) int {
for i, x := range xs {
if x == v { return i } // compile error
}
return -1
}
Hint: not every T supports ==.
Fix:
func Find[T comparable](xs []T, v T) int {
for i, x := range xs {
if x == v { return i }
}
return -1
}
Explanation: with any the compiler does not know T is comparable; the operator is not allowed. Use comparable.
Bug 14 — recursive generic type without parameters in inner reference¶
Hint: same rule as Bug 11.
Fix:
Explanation: when referencing a generic type, you must instantiate it. Inside the body of Tree[T], the natural instantiation is Tree[T].
Bug 15 — using comparable as a regular interface variable¶
Hint: comparable cannot be a parameter type.
Fix: use a generic function:
Explanation: comparable is constraint-only.
Bug 16 — wrong receiver name vs declaration¶
Hint: the receiver renamed T to U. What about the body?
Fix: keep names consistent (or rename T in the signature too):
Explanation: renaming is legal but error-prone. The body must use the new name. The compile error here is undefined: T because inside this method the parameter is named U.
Bug 17 — using a constraint type that is not constrained enough¶
type Numeric interface { int | float64 }
func Avg[T Numeric](xs []T) float64 {
var s T
for _, x := range xs { s += x }
return float64(s) / float64(len(xs)) // compile error?
}
Hint: can you always convert T to float64?
Fix: it actually works for Numeric because both int and float64 convert to float64. But if you change the constraint to include string, it breaks.
Lesson: the conversion float64(s) is only legal if every type in the type set supports it. Adding ~string would break this code.
Bug 18 — concurrency oversight in generic map¶
type Map[K comparable, V any] struct { m map[K]V }
func (m *Map[K, V]) Set(k K, v V) { m.m[k] = v }
func (m *Map[K, V]) Get(k K) (V, bool) { v, ok := m.m[k]; return v, ok }
Hint: not a generics-specific bug, but very common in generic containers.
Fix: add a mutex.
Explanation: generics do not magically make code safe for concurrent use. Document and enforce it explicitly.
Bug 19 — type set with overlapping ~¶
Hint: is int | ~int redundant?
Fix:
Explanation: ~int already includes int. Compilers may not error here, but the union is redundant. Style smell.
Bug 20 — using Stack (uninstantiated) as a function parameter¶
Hint: Stack alone is not a usable type.
Fix: either instantiate or make the function generic.
// Option A — fixed instantiation:
func PrintLen(s Stack[int]) { ... }
// Option B — generic:
func PrintLen[T any](s Stack[T]) { ... }
Explanation: the compiler error is cannot use generic type Stack without instantiation.
Bug 21 — interface type set order matters? (it doesn't — but a related bug)¶
Hint: combining a type set with a value-interface method-element looks odd.
Fix: split the constraint.
Explanation: combining ~int with sort.Interface produces a constraint that says "underlying type is int and has these methods". The methods on int don't include Len, Less, Swap, so the type set after intersection is empty. Sort interface methods belong on a wrapper type, not on a numeric primitive.
Bug 22 — global var of a parameterized type without args¶
Fix:
Explanation: same as Bug 20 — type names must be instantiated.
Bug 23 — interface satisfaction by accident¶
type Pusher[T any] interface { Push(T) }
type IntStack struct{ items []int }
func (s *IntStack) Push(v int) { s.items = append(s.items, v) }
var p Pusher[string] = &IntStack{}
Hint: what is T in Pusher[string]?
Fix: match the type:
Explanation: Push(int) is not the same as Push(string). The compiler error: *IntStack does not implement Pusher[string].
Bug 24 — circular generic type with mismatched constraints¶
type Graph[T any] struct {
nodes map[T]*GraphNode[T] // T is `any`, but map keys must be `comparable`
}
type GraphNode[T any] struct {
value T
edges []*GraphNode[T]
}
Hint: map keys.
Fix: tighten T to comparable:
type Graph[T comparable] struct {
nodes map[T]*GraphNode[T]
}
type GraphNode[T comparable] struct {
value T
edges []*GraphNode[T]
}
Explanation: all related generic types should share consistent constraints.
Bug 25 — capturing a parameter type in a closure assigned to any¶
type Producer[T any] struct{}
func (p *Producer[T]) Make() any {
var t T
return func() T { return t } // type-erases to any
}
Hint: this compiles, but the returned any cannot easily be cast back.
Fix: keep T in the return type:
Explanation: returning any defeats the point of generics. Callers need a type assertion to recover func() T. Almost always wrong.
Summary¶
The most common categories of generic-type bugs:
- Receiver missing type parameter list (
func (s *Stack)instead offunc (s *Stack[T])). - Trying to add method-level type parameters (forbidden).
- Using a constraint interface as a value type.
- Missing
comparablefor map keys. - Missing
~in type sets. - Returning
nilinstead ofvar zero T. - Forgetting that
Stack[int]andStack[string]are different types.
When you see any of these in code review, push back early — they tend to multiply if not caught.
End of find-bug.md.