From edef28d0c8acc7d3bdfaddde8ab27de602cc131b Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Wed, 30 Dec 2020 20:35:21 +0100 Subject: [PATCH] Use a custom error type for invalid lengths, replacing `fmt.Errorf` (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add benchmarks for different kinds of invalid UUIDs Also add a test case for too-short UUIDs to ensure behavior doesn’t change. * Use a custom error type for invalid lengths, replacing `fmt.Errorf` This significantly improves the speed of failed parses due to wrong lengths. Previously the `fmt.Errorf` call dominated, making this the most expensive error and more expensive than successfully parsing: BenchmarkParse-4 29226529 36.1 ns/op BenchmarkParseBadLength-4 6923106 174 ns/op BenchmarkParseLen32Truncated-4 26641954 38.1 ns/op BenchmarkParseLen36Corrupted-4 19405598 59.5 ns/op When the formatting is not required and done on-demand, the failure per se is much faster: BenchmarkParse-4 29641700 36.3 ns/op BenchmarkParseBadLength-4 58602537 20.0 ns/op BenchmarkParseLen32Truncated-4 30664791 43.6 ns/op BenchmarkParseLen36Corrupted-4 18882410 61.9 ns/op --- uuid.go | 10 ++++++++-- uuid_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/uuid.go b/uuid.go index 524404c..daf3639 100644 --- a/uuid.go +++ b/uuid.go @@ -35,6 +35,12 @@ const ( var rander = rand.Reader // random function +type invalidLengthError struct{ len int } + +func (err *invalidLengthError) Error() string { + return fmt.Sprintf("invalid UUID length: %d", err.len) +} + // Parse decodes s into a UUID or returns an error. Both the standard UUID // forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the @@ -68,7 +74,7 @@ func Parse(s string) (UUID, error) { } return uuid, nil default: - return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) + return uuid, &invalidLengthError{len(s)} } // s is now at least 36 bytes long // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx @@ -112,7 +118,7 @@ func ParseBytes(b []byte) (UUID, error) { } return uuid, nil default: - return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) + return uuid, &invalidLengthError{len(b)} } // s is now at least 36 bytes long // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx diff --git a/uuid_test.go b/uuid_test.go index 4e04e7c..709e34c 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -517,6 +517,15 @@ func TestRandomFromReader(t *testing.T) { } } +func TestWrongLength(t *testing.T) { + _, err := Parse("12345") + if err == nil { + t.Errorf("expected ‘12345’ was invalid") + } else if err.Error() != "invalid UUID length: 5" { + t.Errorf("expected a different error message for an invalid length") + } +} + var asString = "f47ac10b-58cc-0372-8567-0e02b2c3d479" var asBytes = []byte(asString) @@ -595,3 +604,33 @@ func BenchmarkUUID_URN(b *testing.B) { } } } + +func BenchmarkParseBadLength(b *testing.B) { + short := asString[:10] + for i := 0; i < b.N; i++ { + _, err := Parse(short) + if err == nil { + b.Fatalf("expected ‘%s’ was invalid", short) + } + } +} + +func BenchmarkParseLen32Truncated(b *testing.B) { + partial := asString[:len(asString)-4] + for i := 0; i < b.N; i++ { + _, err := Parse(partial) + if err == nil { + b.Fatalf("expected ‘%s’ was invalid", partial) + } + } +} + +func BenchmarkParseLen36Corrupted(b *testing.B) { + wrong := asString[:len(asString)-1] + "x" + for i := 0; i < b.N; i++ { + _, err := Parse(wrong) + if err == nil { + b.Fatalf("expected ‘%s’ was invalid", wrong) + } + } +}