I have a function that should be comparing two generic types. If they have Equal implemented, this should be preferred. Here is my code:
type EqualChecker interface {
Equal(EqualChecker) bool
}
//AssertEq adds error to test with stack trace when provided arguments are not equal
//If values of Pointers should be compared use AssertEqPtr
func AssertEq[C comparable](left C, right C, t *testing.T) {
leftE, ok := left.(EqualChecker)
if ok {
rightE, _ := right.(EqualChecker)
if !leftE.Equal(rightE) {
t.Errorf("Not equal: \n left: %+v\nright: %+v\n%s", left, right, debug.Stack())
}
return
}
if left != right {
t.Errorf("Not equal: \n left: %+v\nright: %+v\n%s", left, right, debug.Stack())
}
}
But I get the compilation error:
invalid operation: cannot use type assertion on type parameter value left (variable of type C constrained by comparable)
I don’t know anything about generics, but type assertions only work when the expression is an interface type. For example, you can’t do:
var a string = "test"
b := a.(interface{})
Because a is a concrete string type. Perhaps the issue is that there’s nothing in the type constraint to ensure C is an interface type. Again, I don’t know anything about generics, so I’m not sure how to fix it. Is there a constraint to require that a type be an interface type?
Create a parametric interface, so that we can define a function Equal(T) without having to name its parameter.
type Equaler[T any] interface {
Equal(T) bool
}
Function AssertEq gets parametrized as AssertEq[T Equaler[T]], because:
Type constraints are interfaces, hence we can use Equaler here, and
The Equaler interface itself is a parametrized interface, hence we use Equaler[T] here.
func AssertEq[T Equaler[T]](left, right T, t *testing.T) {
if !left.Equal(right) {
t.Errorf("Not equal:\nleft: %+v\nright: %+v\n", left, right)
}
return
}
The example struct type AmIEqual implements interface Equaler[T] by defining a method Equal().
type AmIEqual struct {
value int
}
func (e AmIEqual) Equal(other AmIEqual) bool {
return e.value == other.value
}
Now we can instantiate AmIEqual values and pass them to AssertEq().
Thanks to type inference, there is no need to call AssertEq like AssertEq[AmIEqual](a, b, t), although the latter would work, too.
func TestAssert(t *testing.T) {
a := AmIEqual{value: 1}
b := AmIEqual{value: 1}
AssertEq[AmIEqual](a, b, t) // PASS
// comment the two lines below to get a PASS
c := AmIEqual{value: 2}
AssertEq(a, c, t) // FAIL
}
Thanks for your very good reply but sadly your solution is missing my main point.
I explicitly want to accept any type that is comparable. If in addition the Equal method is implemented, I’d like to prefer that one.
Example:
I want to be able to just pass two int types and compare them with == or !=
If I pass a Time for example, I want to check if the dates are equal independent of the timezone. So I need to use .Equal there instead of == or !=
Your solution works if I want to restrict use of function to just types that have Equal implemented.
If the type is not that hard limited you could ask what generic types are used for in this example. The reason is, that the function only makes sence if both passed arguments have the same type and are comparable in some way