Skip to content

Commit

Permalink
Use a custom error type for invalid lengths, replacing fmt.Errorf (#69
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
joewreschnig committed Dec 30, 2020
1 parent 0e4e311 commit edef28d
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 2 deletions.
10 changes: 8 additions & 2 deletions uuid.go
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions uuid_test.go
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
}
}

0 comments on commit edef28d

Please sign in to comment.