Skip to content

Commit fef5f1a

Browse files
committed
markers: delegate to an Is() method if present
The Go std `errors` package has this rule about `Is()`: > An error is considered to match a target if it is equal to that > target or if it implements a method Is(error) bool such that > Is(target) returns true. ([source](https://golang.org/pkg/errors/?source=post_page---------------------------#Is)) This patch extends the errors library accordingly. Caveat: the doc says "if a.Is(b) is true, then errors.Is(a, b) is true". This is the behavior implemented here. The doc does not say "if a.Is(b) is false, then errors.Is(a, b) is false". The CockroachDB errors library uses error identity markers which enable comparisons across the network. The identity marker includes only the type and message. Therefore, it is not possible to "make two errors unequal" by customizing an `Is()` method to return `false`, if their network identity marker is otherwise equal. To change this behavior, use the `errors.Mark()` API instead, or implement an `ErrorTypeKey()` method.
1 parent a9df884 commit fef5f1a

File tree

3 files changed

+68
-1
lines changed

3 files changed

+68
-1
lines changed

markers/markers.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,22 @@ import (
2828
// Is determines whether one of the causes of the given error or any
2929
// of its causes is equivalent to some reference error.
3030
//
31+
// As in the Go standard library, an error is considered to match a
32+
// reference error if it is equal to that target or if it implements a
33+
// method Is(error) bool such that Is(reference) returns true.
34+
//
35+
// Note: the inverse is not true - making an Is(reference) method
36+
// return false does not imply that errors.Is() also returns
37+
// false. Errors can be equal because their network equality marker is
38+
// the same. To force errors to appear different to Is(), use
39+
// errors.Mark().
40+
//
3141
// Note: if any of the error types has been migrated from a previous
3242
// package location or a different type, ensure that
3343
// RegisterTypeMigration() was called prior to Is().
3444
func Is(err, reference error) bool {
3545
if reference == nil {
36-
return err == reference
46+
return err == nil
3747
}
3848

3949
// Direct reference comparison is the fastest, and most
@@ -42,6 +52,11 @@ func Is(err, reference error) bool {
4252
if c == reference {
4353
return true
4454
}
55+
// Compatibility with std go errors: if the error object itself
56+
// implements Is(), try to use that.
57+
if tryDelegateToIsMethod(c, reference) {
58+
return true
59+
}
4560
}
4661

4762
if err == nil {
@@ -67,6 +82,13 @@ func Is(err, reference error) bool {
6782
return false
6883
}
6984

85+
func tryDelegateToIsMethod(err, reference error) bool {
86+
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(reference) {
87+
return true
88+
}
89+
return false
90+
}
91+
7092
// HasType returns true iff err contains an error whose concrete type
7193
// matches that of referenceType.
7294
func HasType(err error, referenceType error) bool {
@@ -125,6 +147,11 @@ func IsAny(err error, references ...error) bool {
125147
if c == refErr {
126148
return true
127149
}
150+
// Compatibility with std go errors: if the error object itself
151+
// implements Is(), try to use that.
152+
if tryDelegateToIsMethod(c, refErr) {
153+
return true
154+
}
128155
}
129156
if c == nil {
130157
// This special case is to support a comparison to a nil

markers/markers_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,3 +569,33 @@ type invalidError struct {
569569

570570
func (e *invalidError) Error() string { return e.emptyRef.Error() }
571571
func (e *invalidError) Cause() error { return e.emptyRef }
572+
573+
func TestDelegateToIsMethod(t *testing.T) {
574+
tt := testutils.T{T: t}
575+
576+
efoo := &errWithIs{msg: "foo", seecret: "foo"}
577+
efoo2 := &errWithIs{msg: "foo", seecret: "bar"}
578+
ebar := &errWithIs{msg: "bar", seecret: "foo"}
579+
580+
tt.Check(markers.Is(efoo, efoo2)) // equality based on message
581+
tt.Check(markers.Is(efoo, ebar)) // equality based on method
582+
tt.Check(!markers.Is(efoo2, ebar)) // neither msg nor method
583+
584+
tt.Check(markers.IsAny(efoo, efoo2, ebar))
585+
tt.Check(markers.IsAny(efoo2, ebar, efoo))
586+
tt.Check(!markers.IsAny(efoo2, ebar, errors.New("other")))
587+
}
588+
589+
type errWithIs struct {
590+
msg string
591+
seecret string
592+
}
593+
594+
func (e *errWithIs) Error() string { return e.msg }
595+
596+
func (e *errWithIs) Is(o error) bool {
597+
if ex, ok := o.(*errWithIs); ok {
598+
return e.seecret == ex.seecret
599+
}
600+
return false
601+
}

markers_api.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ import "github.com/cockroachdb/errors/markers"
1919
// Is determines whether one of the causes of the given error or any
2020
// of its causes is equivalent to some reference error.
2121
//
22+
// As in the Go standard library, an error is considered to match a
23+
// reference error if it is equal to that target or if it implements a
24+
// method Is(error) bool such that Is(reference) returns true.
25+
//
26+
// Note: the inverse is not true - making an Is(reference) method
27+
// return false does not imply that errors.Is() also returns
28+
// false. Errors can be equal because their network equality marker is
29+
// the same. To force errors to appear different to Is(), use
30+
// errors.Mark().
31+
//
2232
// Note: if any of the error types has been migrated from a previous
2333
// package location or a different type, ensure that
2434
// RegisterTypeMigration() was called prior to Is().

0 commit comments

Comments
 (0)