In Go 1.18 and Go 1.19 I can ensure at compile time that a type is strictly comparable, i.e. it supports == and != operators and those are guaranteed to not panic at run time.
This is useful for example to avoid inadvertently adding fields to a struct that could cause unwanted panics.
I just attempt to instantiate comparable with it:
// supports == and != but comparison could panic at run time
type Foo struct {
SomeField any
}
func ensureComparable[T comparable]() {
// no-op
}
var _ = ensureComparable[Foo] // doesn't compile because Foo comparison may panic
This is possible in Go 1.18 and 1.19 due to the very definition of the comparable constraint:
The predeclared interface type comparable denotes the set of all non-interface types that are comparable
Even though the Go 1.18 and 1.19 spec fail to mention types that are not interfaces but also not strictly comparable, e.g. [2]fmt.Stringer or struct { foo any }, the gc compiler does reject these as arguments for comparable.
Playground with several examples: https://go.dev/play/p/_Ggfdnn6OzZ
With Go 1.20, instantiating comparable will be aligned with the broader notion of comparability. This makes ensureComparable[Foo] compile even though I don't want it to.
Is there a way to statically ensure strict comparability with Go 1.20?
To test that
Foois strictly comparable in Go 1.20, instantiateensureComparablewith a type parameter constrained byFoo.This solution has been originally suggested by Robert Griesemer here.
So how does it work?
Go 1.20 introduces a difference between implementing an interface and satisfying a constraint:
The second bullet point is the exception that permits interfaces, and types with interfaces, to instantiate
comparable.So now in Go 1.20 the type
Fooitself can instantiatecomparabledue to the satisfiability exception. But the type parameterTisn'tFoo. Comparability of type parameters is defined differently:The type set of
Tincludes a typeFoothat is not strictly comparable (because it has an interface field), thereforeTdoesn't satisfycomparable. Even thoughFooitself does.This trick effectively makes the program fail to compile if
Foo's operators==and!=might panic at run time.