A Zero-Sized Bug Hunt in golang.org/x/sync
Categories:
Series:
… continued from the previous post.
While researching the usage of zero-sized types in Go I wrote zerolint1 and the accompanying cmplint2 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
panicin a Group.Do is caught and re-thrown in the enclosing scope.The value returned from
recoveris always anerrortype.If the value passed to
panicis also anerror, the result fromrecoverwraps 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.
wrappedErrorTypeis alwaysfalse(should betruein 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
packagetosingleflight_testandimport . "golang.org/x/sync/singleflight"to avoid using internal types. Skip this when you believe dot imports are evil.Line 65
tc := tcis 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 Ts. 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.