A Zero-Sized Bug Hunt in golang.org/x/sync
… 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 anerror
type.If the value passed to
panic
is also anerror
, the result fromrecover
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 alwaysfalse
(should betrue
in the second case) and the values are already wrapped in the private error (they shouldn’t):36 37 38 39 40 41 42 43 44 45
{ name: "panicError wraps non-error type", panicValue: "string value", wrappedErrorType: false, }, { name: "panicError wraps error type", panicValue: new(errValue), wrappedErrorType: true, },
We should probably change the
package
tosingleflight_test
andimport . "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.223Also, 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 sentinelerrValue
.
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.