All Benchmarks

Go Benchmark

Regexp vs Manual String Validation

Precompiled regexp, inline regexp, and hand-rolled string checks compared.

regexpregexstringsvalidationemailhot-loop

Validating that a string "looks like an email" is a classic task with three common solutions in Go: a regular expression compiled once at package level, the same regexp carelessly compiled inside the hot loop, and a hand-written validator built from strings functions. This benchmark runs all three against the same mix of valid and invalid addresses. Two lessons hide in the chart: compiling a regexp is orders of magnitude more expensive than matching one (never call regexp.MustCompile in a loop), and even a precompiled regexp costs several times more than a few explicit strings.IndexByte checks — the price of the regex engine's generality.

linux/amd64AMD Ryzen 9 9950X3D 16-Core Processorbenchmarks/regexp-vs-manual
Compare atCPUs

1 CPU

Fastest

Manual Validation

4.71 ns/op

Slowest

Inline Regexp

2.89 µs/op · 613x slower

32 CPUs

Fastest

Manual Validation

4.63 ns/op

Slowest

Inline Regexp

3.49 µs/op · 754x slower

Performance Comparison (lower is better)
CPU:
#1

Manual Validation #

Fastest

A hand-rolled validator using strings.IndexByte and strings.LastIndexByte to check the local@domain.tld shape directly. Zero allocations and a handful of byte scans. Less expressive and more code to maintain than a regexp, but for simple shapes on hot paths it is by far the fastest option.

CPU Scaling (lower is better)
// emailPattern matches a simple email-ish shape: local@domain.tld
const emailPattern = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`

// candidates returns a mix of valid and invalid email addresses.
func candidates() []string {
	return []string{
		"gopher@example.com",
		"jane.doe+news@mail.example.co.uk",
		"not-an-email",
		"missing@tld",
		"@example.com",
		"trailing@example.",
	}
}

// isValidEmail checks the same email-ish shape as emailPattern
// using plain string functions: local@domain.tld
func isValidEmail(s string) bool {
	at := strings.IndexByte(s, '@')
	if at <= 0 || at >= len(s)-1 {
		return false // no @, empty local part, or empty domain
	}
	domain := s[at+1:]
	if strings.IndexByte(domain, '@') >= 0 {
		return false // more than one @
	}
	dot := strings.LastIndexByte(domain, '.')
	// The dot must exist, not lead the domain, and leave a TLD of 2+ chars.
	return dot > 0 && len(domain)-dot-1 >= 2
}

func BenchmarkManualValidation_run(b *testing.B) {
	emails := candidates()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sink = isValidEmail(emails[i%len(emails)])
	}
}
1 CPU
612.9xfaster(61186%)thanInline Regexp
35.4xfaster(3441%)thanPrecompiled Regexp
32 CPUs
753.5xfaster(75251%)thanInline Regexp
35.7xfaster(3473%)thanPrecompiled Regexp
#2

Precompiled Regexp #

Compiles regexp.MustCompile(emailPattern) once before the loop — the idiomatic equivalent of a package-level var emailRe = regexp.MustCompile(...). Each check only pays for running the regex engine's state machine over the input.

CPU Scaling (lower is better)
// emailPattern matches a simple email-ish shape: local@domain.tld
const emailPattern = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`

// candidates returns a mix of valid and invalid email addresses.
func candidates() []string {
	return []string{
		"gopher@example.com",
		"jane.doe+news@mail.example.co.uk",
		"not-an-email",
		"missing@tld",
		"@example.com",
		"trailing@example.",
	}
}

func BenchmarkPrecompiledRegexp_run(b *testing.B) {
	// Compiled once, outside the hot loop.
	re := regexp.MustCompile(emailPattern)
	emails := candidates()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sink = re.MatchString(emails[i%len(emails)])
	}
}
1 CPU
17.3xfaster(1631%)thanInline Regexp
35.4xslower(3441%)thanManual Validation
32 CPUs
21.1xfaster(2009%)thanInline Regexp
35.7xslower(3473%)thanManual Validation
#3

Inline Regexp #

Slowest

Calls regexp.MustCompile(emailPattern).MatchString(...) inside the loop, so every iteration parses the pattern, builds the state machine, and allocates the Regexp object before matching. A surprisingly common mistake — and the chart shows why it matters.

CPU Scaling (lower is better)
// emailPattern matches a simple email-ish shape: local@domain.tld
const emailPattern = `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`

// candidates returns a mix of valid and invalid email addresses.
func candidates() []string {
	return []string{
		"gopher@example.com",
		"jane.doe+news@mail.example.co.uk",
		"not-an-email",
		"missing@tld",
		"@example.com",
		"trailing@example.",
	}
}

func BenchmarkInlineRegexp_run(b *testing.B) {
	emails := candidates()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		// Re-compiles the pattern on every single call.
		sink = regexp.MustCompile(emailPattern).MatchString(emails[i%len(emails)])
	}
}
1 CPU
612.9xslower(61186%)thanManual Validation
17.3xslower(1631%)thanPrecompiled Regexp
32 CPUs
753.5xslower(75251%)thanManual Validation
21.1xslower(2009%)thanPrecompiled Regexp

Contributors