Type Inference — Find the Bug¶
Fifteen-plus snippets where type inference subtly fails or gives a surprising result. For each: read the code, hunt the bug, peek at the hint, then check the fix and explanation.
Bug 1 — The vanishing T¶
package main
import "fmt"
func Build[T any]() T {
var z T
return z
}
func main() {
v := Build()
fmt.Println(v)
}
Hint. Where does T appear in the signature?
Fix.
or restructure:Explanation. T only appears in the return type. FTAI has nothing to look at. Inference fails with "cannot infer T".
Bug 2 — fmt.Sprint in Map¶
func Map[T, U any](s []T, f func(T) U) []U {
out := make([]U, len(s))
for i, v := range s { out[i] = f(v) }
return out
}
nums := []int{1, 2, 3}
strs := Map(nums, fmt.Sprint)
Hint. What is the actual type of fmt.Sprint?
Fix.
Explanation. fmt.Sprint is func(...any) string. The compiler tries to unify func(T) U with func(...any) string and fails on arity (variadic vs fixed). Wrapping in a closure with the right signature fixes it.
Bug 3 — Wrong default type¶
func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
acc := init
for _, v := range s { acc = f(acc, v) }
return acc
}
events := []Event{ /* ... */ }
total := Reduce(events, 0, count)
if total > math.MaxInt32 { /* never true */ }
Hint. What is the type of 0?
Fix.
Explanation. 0 is an untyped int constant, defaulting to int. On 32-bit platforms int is int32, so the comparison is always false. Pin the accumulator type explicitly.
Bug 4 — Nil with no anchor¶
Hint. What does nil tell you about T?
Fix.
Explanation. Bare nil carries no type information. Inference cannot proceed.
Bug 5 — Conflicting bindings¶
Hint. What is the type of user.ID?
Fix.
Explanation. If user.ID is UserID (a defined type) and "u-42" defaults to string, unification cannot agree on T. Convert one side, or define a constructor.
Bug 6 — Named slice type rejected¶
type IDs []int
func Sum[E int | float64](s []E) E {
var total E
for _, v := range s { total += v }
return total
}
ids := IDs{1, 2, 3}
total := Sum(ids)
Hint. Is IDs the same as []int for inference purposes?
Fix.
Explanation. Without ~[]E, only the exact type []int is acceptable, not IDs. The fix is to use ~[]E so named slice types are accepted.
Bug 7 — Missing comparable¶
func Index[E any](s []E, target E) int {
for i, v := range s {
if v == target { return i } // compile error: == on E
}
return -1
}
Hint. What does E any permit?
Fix.
Explanation. any does not allow ==. Use comparable. This is a constraint bug, but it shows up at the inference stage because the compiler cannot proceed past the function body type-check.
Bug 8 — Variadic with no values¶
func Sum[T int | float64](xs ...T) T {
var total T
for _, v := range xs { total += v }
return total
}
zero := Sum()
Hint. What does the compiler see in the variadic args?
Fix.
Explanation. No arguments means nothing to unify. Provide a type explicitly or pass at least one element.
Bug 9 — Inference defaults to int when you wanted int64¶
Hint. Where is T carried by the call?
Fix.
Explanation. T is in the return type only. Inference fails. The bug is that the user assumed T = byte would magically be picked.
Bug 10 — Method value loses receiver type¶
type Repo struct{}
func (r *Repo) Get(id string) (User, error) { /* ... */ }
func Apply[T, U any](x T, f func(T) (U, error)) (U, error) { return f(x) }
var r *Repo
u, _ := Apply("u-1", r.Get)
Hint. This works in 1.21+. What about earlier?
Fix. Bump go.mod to 1.21+. If you must support 1.18:
Explanation. Function-shape unification on method values was unreliable before 1.21.
Bug 11 — Constraint without core type¶
type Pair interface { ~int | ~string }
func Bag[T Pair, S ~[]T](xs S) S { return xs }
xs := []int{1, 2, 3}
out := Bag(xs)
Hint. Can constraint inference walk through Pair?
Fix. It actually compiles fine in 1.21+ because FTAI binds T = int and S = []int directly. But:
Explanation. Constraint inference is not needed when FTAI alone binds everything. The bug is the assumption that order or core types are problems here — they are not.
Bug 12 — Untyped string ambiguity¶
type Slug string
func Concat[T ~string](a, b T) T { return a + b }
var s Slug = "hi"
out := Concat(s, "world")
Hint. Is "world" representable as Slug?
Fix. This works in 1.21+; it may fail in older versions.
Explanation. Untyped string "world" must be representable as Slug. The improved 1.21 inference correctly performs the conversion. Older versions sometimes refused.
Bug 13 — Generic function value assigned¶
Hint. Map is a generic function. Can it be a value?
Fix.
Explanation. A generic function must be instantiated before being used as a first-class value.
Bug 14 — Mixed numeric types in inference¶
func Min[T int | float64](a, b T) T {
if a < b { return a }
return b
}
var x int32 = 5
result := Min(x, 10)
Hint. Is int32 in the type set?
Fix.
result := Min(int(x), 10)
// or expand the constraint
func Min[T int | int32 | float64](a, b T) T { /* ... */ }
Explanation. int32 is not a member of int | float64. The typed argument forces T = int32, which then fails the constraint check.
Bug 15 — Pointer to value not pointer to T¶
Hint. Two arguments, both involve T. What does each say?
Fix.
Explanation. From &u the compiler infers T = User. From "hi" it infers T = string. Conflict. Inference fails. Make the second argument a User.
Bug 16 — Inference of E from any-typed slice¶
func First[E any](s []E) E { return s[0] }
xs := []any{1, "x", true}
v := First(xs)
fmt.Printf("%T\n", v) // ?
Hint. What does E become?
Fix. Compiles, but v is any. To get int:
Explanation. E = any. The bug is the assumption that First could pick the dynamic type at index 0. It cannot — generics are static.
Bug 17 — Map key type collision¶
func Invert[K, V comparable](m map[K]V) map[V]K {
out := make(map[V]K, len(m))
for k, v := range m { out[v] = k }
return out
}
byID := map[int][]string{1: {"a"}, 2: {"b"}}
inv := Invert(byID)
Hint. Is V comparable here?
Fix. []string is not comparable; pick a different value representation.
Explanation. Constraint check fails — []string is not comparable. The error happens after FTAI, at constraint validation.
Bug 18 — Channel direction mismatch¶
func Drain[T any](ch <-chan T) []T {
var out []T
for v := range ch { out = append(out, v) }
return out
}
ch := make(chan int)
out := Drain(ch)
Hint. Does chan int match <-chan T?
Fix. It works — Go converts chan T to <-chan T implicitly. The "bug" is in the opposite direction:
recv := make(<-chan int)
out := Drain(recv) // fine
sender := make(chan<- int)
Drain(sender) // fails: cannot use chan<- as <-chan
Explanation. Channel direction matters for unification. Send-only cannot be passed as receive-only.
Bug 19 — Reduce reset on each call¶
func Reduce[T, U any](s []T, init U, f func(U, T) U) U { /* ... */ }
total := Reduce([]int{1, 2, 3}, 0, func(acc, x int) int { return acc + x })
total += Reduce([]int{4, 5, 6}, 0, func(acc, x int) int { return acc + x })
Hint. This is not strictly an inference bug — but the 0 is.
Explanation. Each 0 is int. For long-running aggregations across calls you may want int64:
var total int64
total += Reduce([]int{1, 2, 3}, int64(0), func(acc int64, x int) int64 { return acc + int64(x) })
Bug 20 — Generic builtin assumption¶
Hint. Is make generic?
Fix. Provide a concrete type.
Explanation. make is a builtin, not a generic function. It does not participate in user-level type inference; you must always specify the type.
Self-Check¶
For each bug above: - Could you predict the failure without running the code? - Did you reach for the right fix on the first try? - Could you write a unit test that pins the correct behaviour?
If yes to all three, you have a working professional understanding of where Go's inference breaks down and how to recover.