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 Foo
is strictly comparable in Go 1.20, instantiate ensureComparable
with a type parameter constrained by Foo
.
// unchanged
type Foo struct {
SomeField any
}
// unchanged
func ensureComparable[T comparable]() {}
// T constrained by Foo, instantiate ensureComparable with T
func ensureStrictlyComparable[T Foo]() {
_ = ensureComparable[T] // <---- doesn't compile
}
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:
A type
T
satisfies a constraintC
if
T
implementsC
; orC
can be written in the forminterface{ comparable; E }
, whereE
is a basic interface andT
is comparable and implementsE
.
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 Foo
itself can instantiate comparable
due to the satisfiability exception. But the type parameter T
isn't Foo
. Comparability of type parameters is defined differently:
Type parameters are comparable if they are strictly comparable (see below).
[...]
Type parameters are strictly comparable if all types in their type set are strictly comparable.
The type set of T
includes a type Foo
that is not strictly comparable (because it has an interface field), therefore T
doesn't satisfy comparable
. Even though Foo
itself does.
This trick effectively makes the program fail to compile if Foo
's operators ==
and !=
might panic at run time.