Type Constraints — Find the Bug¶
Table of Contents¶
Each bug has: Code, Hint, Fix, Explanation. Try to spot the bug before reading the hint.
How to Use This File¶
- Read the code.
- Predict the compile error or unexpected behavior.
- Check your hint.
- Read the fix.
- Read the explanation.
This drills the most common constraint mistakes. After finishing all 15 you'll catch them on sight in code review.
Bugs 1-15¶
Bug 1 — Missing ~¶
package main
type Number interface {
int | float64
}
func Sum[T Number](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
type UserID int
func main() {
ids := []UserID{1, 2, 3}
Sum(ids) // (?) what happens
}
Hint. What is the relationship between UserID and int?
Fix.
Explanation. Without ~, the constraint matches only the predeclared int. UserID is a distinct defined type (even though its underlying type is int) and is rejected. Add ~ to admit the family of int-shaped types.
Bug 2 — Wrong union with intersection intent¶
type WeirdConstraint interface {
int
string
}
func F[T WeirdConstraint]() {}
func main() {
F[int]() // (?)
F[string]() // (?)
}
Hint. Semicolon between elements means intersection, not union.
Fix.
Explanation. interface{ int; string } requires the type to be in both type sets. No type is both int and string. The intersection is empty; every instantiation fails. To express "either", use the | operator inside a single type element.
Bug 3 — Embedding a method-only interface as a type element¶
type Stringer interface { String() string }
type WrongConstraint interface {
Stringer | int // (?) — error here
}
Hint. What kinds of operands does the union operator accept?
Fix.
type WrongConstraint interface {
int // type element
String() string // method element (separately)
}
Explanation. | operates only on type terms (types or ~T). You cannot union an interface (method-only or general) with a type term. To require both "method String()" and "type int", list them as separate interface elements (intersection).
Bug 4 — Trying to use a general interface as a value¶
Hint. Can a general interface be used outside the constraint position?
Fix. Don't use it as a value. If you genuinely need a runtime value, define a method-only interface instead:
Explanation. General interfaces (interfaces with type elements) describe a compile-time type set. They cannot be implemented by a runtime value because their identity is restricted by ~T or T | U. The compiler rejects var x Number.
Bug 5 — Calling == under any¶
Hint. Does any guarantee comparability?
Fix.
Explanation. any is the universal type set; it includes types like slices that don't support ==. The compiler refuses == on a type parameter unless the constraint guarantees comparability (comparable or a type-element union of comparable types).
Bug 6 — Forgetting comparable for map keys¶
Hint. What constraint do Go map keys require?
Fix.
Explanation. Go's map key type must be comparable. The type parameter K must satisfy comparable for the compiler to accept the map type definition.
Bug 7 — Method on pointer receiver, constraint requires the method¶
type Resetter interface {
Reset()
}
type Counter struct { n int }
func (c *Counter) Reset() { c.n = 0 }
func ResetAll[T Resetter](xs []T) {
for _, x := range xs {
x.Reset()
}
}
func main() {
counters := []Counter{ {n: 1}, {n: 2} }
ResetAll(counters) // (?)
}
Hint. What is the method set of Counter vs *Counter?
Fix.
Or, define Reset() with a value receiver (only sensible if it doesn't need to mutate; here it does, so pointers it is).
Explanation. Counter does not have Reset() in its method set; only *Counter does. The constraint Resetter is not satisfied by Counter. Pass pointers.
Bug 8 — ~T on a defined type¶
Hint. What does the spec say about the type after ~?
Fix.
Explanation. The type after ~ must be its own underlying type — typically a predeclared type or an unnamed type literal. ~Money is illegal because Money is a defined type. Use ~int to match Money and friends.
Bug 9 — Using ~ on a slice in a constraint with a method element¶
type WrongBytes interface {
~[]byte
Len() int
}
type Payload []byte
func main() {
var p Payload
UseBytes(p) // (?) — error
}
func UseBytes[T WrongBytes](x T) {
println(x.Len())
}
Hint. Does []byte have a Len() method?
Fix. Either drop the method element or use a type that has it:
Then UseBytes(p) works.
Explanation. The constraint requires both the underlying type to be []byte and the type to have a Len() int method. Bare []byte and Payload don't have it unless you define it.
Bug 10 — Over-restrictive type element¶
type Numeric interface {
int | int64 | float64 // (?)
}
func Sum[T Numeric](xs []T) T {
var s T
for _, x := range xs { s += x }
return s
}
func main() {
Sum([]int32{1, 2, 3}) // (?) — error
}
Hint. Is int32 in the type set?
Fix.
type Numeric interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64
}
// or just: import "golang.org/x/exp/constraints"; type Numeric interface { constraints.Integer | constraints.Float }
Explanation. A union with three types is too narrow. Either enumerate all numeric kinds or use constraints.Integer | constraints.Float.
Bug 11 — comparable over-broad assumption pre-1.20¶
Hint. What did comparable mean before Go 1.20?
Fix. Either upgrade to Go 1.20+, or define a narrower constraint:
Explanation. Before Go 1.20, comparable did not include interface types. any is interface{}, so Set[any] was illegal. Go 1.20 relaxed this; Set[any] now compiles, but == on values that hold non-comparable dynamic types panics.
Bug 12 — Mixed type set blocks an operator¶
type IntOrSlice interface { int | []int }
func Double[T IntOrSlice](x T) T {
return x + x // (?) — error
}
Hint. Does every type in the union support +?
Fix. Choose one shape:
Or write two functions — one per shape.
Explanation. + is supported by int but not by []int. The compiler refuses the operator because not every type in the type set supports it. There is no way to "specialize per branch" inside a single generic body.
Bug 13 — Empty type set after intersection¶
type StringInt interface {
~int
~string
}
func F[T StringInt](x T) {}
func main() {
F[int]("hi") // (?)
}
Hint. What is the intersection of ~int and ~string?
Fix. Use a union:
Explanation. ~int and ~string describe disjoint families. Their intersection is empty; no type satisfies both. The constraint accepts the declaration but rejects every type argument.
Bug 14 — Trying to embed a constraint into a value-typed interface¶
type Numeric interface {
~int | ~float64
}
type Foo interface {
Numeric
String() string
}
func main() {
var f Foo // (?)
_ = f
}
Hint. Embedding a general interface into another interface — what does it produce?
Fix. If you need a runtime-valuable interface, drop the type element:
If you need a constraint, that's fine — but use it only in [T Foo] positions.
Explanation. Embedding Numeric (a general interface) makes Foo a general interface too. General interfaces cannot be used as value types. var f Foo is rejected.
Bug 15 — Re-declaring comparable¶
Hint. comparable is a predeclared identifier.
Fix. Pick a different name:
Explanation. comparable is reserved as a predeclared identifier (alongside any, int, etc.). You cannot shadow it with your own type definition without confusion. Pick a different, descriptive name like Hashable or MapKey.
Summary¶
The bugs above cover the recurring constraint mistakes: - Forgetting ~. - Confusing union and intersection. - Mixing method elements and type elements incorrectly. - Using a general interface as a value. - Wrong receiver type for a method-element constraint. - Empty/unsatisfiable type sets.
When reviewing constraint code, run through this list. The cost is two minutes; the savings can be hours of debugging mysterious "T does not satisfy" errors.