… continued from the previous post.
While researching the usage of zero-sized types in Go I wrote zerolint
1 and the accompanying cmplint
2 and
examined over 500 popular Go projects.
I want to look into some examples why I think those linters are useful. Let’s start with golang.org/x/sync/singleflight:
git clone --branch v0.15.0 https://go.googlesource.com/sync golang-sync
cd golang-sync
zerolint ./singleflight
# .../golang-sync/singleflight/singleflight_test.go:24:11: error interface implemented on pointer to zero-sized type "golang.org/x/sync/singleflight.errValue" (zl:err)
# .../golang-sync/singleflight/singleflight_test.go:78:8: comparison of pointer to zero-size type "golang.org/x/sync/singleflight.errValue" with error interface (zl:cme)
Okay, interesting. Let’s see:
|
|
This test should verify that
A panic
in a Group.Do is caught and re-thrown in the
enclosing scope.
The value returned from recover
is always an error
type.
If the value passed to panic
is also an error
, the result from recover
wraps the value.
The last point is why this test is named TestPanicErrorUnwrap
.
Let us clean up a little before we come to the point:
The test cases are erroneous. wrappedErrorType
is always false
(should be true
in the second case) and the
values are already wrapped in the private error (they shouldn’t):
|
|
We should probably change the package
to singleflight_test
and import . "golang.org/x/sync/singleflight"
to
avoid using internal types. Skip this when you believe dot
imports are evil.
Line 65 tc := tc
is not needed since Go 1.223
Also, line 79 should be something like
t.Errorf("unexpected wrapped error \"%v\"; want \"%v\"", err, new(errValue))
, since the error type we get is
internal, so we can’t directly expect that - it should only wrap our error.
Let’s run the modified test (Go Playground):
=== RUN TestPanicErrorUnwrap
=== RUN TestPanicErrorUnwrap/panicError_wraps_non-error_type
=== RUN TestPanicErrorUnwrap/panicError_wraps_error_type
--- PASS: TestPanicErrorUnwrap (0.00s)
--- PASS: TestPanicErrorUnwrap/panicError_wraps_non-error_type (0.00s)
--- PASS: TestPanicErrorUnwrap/panicError_wraps_error_type (0.00s)
PASS
Fine, we are done? Just for the fun of it, let’s modify line 10 to type errValue struct{ _ int }
and run it again
(Go Playground):
=== RUN TestPanicErrorUnwrap
=== RUN TestPanicErrorUnwrap/panicError_wraps_non-error_type
=== RUN TestPanicErrorUnwrap/panicError_wraps_error_type
--- FAIL: TestPanicErrorUnwrap (0.00s)
--- PASS: TestPanicErrorUnwrap/panicError_wraps_non-error_type (0.00s)
--- FAIL: TestPanicErrorUnwrap/panicError_wraps_error_type (0.00s)
FAIL
So, when you read the last article you’ll know that errors.Is(err, new(T))
is undefined for
zero-sized T
s. Let us first fix the test:
Use a (local) sentinel value errValue := errors.New("error value")
instead of the type.
Replace all new(errValue)
by references to the sentinel errValue
.
The fixed test4 runs (Go Playground), is correct, not flagged by zerolint
and
easier to understand.
As a closing remark: zerolint
(and in this case cmplint
) find real errors in popular Go programs that are not
necessarily detectable by testing.
… to be continued.