0

One Regex, Eight Engines: What Actually Changes When You Switch Languages

You wrote a regex in Python. It works. You paste the same pattern into a Go service and the program refuses to even compile it. Nothing about your pattern changed — but the engine did.

Most tutorials quietly assume one regex dialect. In production you meet eight of them: PCRE2, RE2, ECMAScript (JavaScript), Python re, Go regexp, Java, .NET, and Rust's regex crate. They disagree on syntax, on features, and on how fast they fail.

The three families

Backtracking engines — PCRE2, JavaScript, Python, Java, .NET. They try a match, and when it fails they backtrack and retry. That is what gives you lookarounds and backreferences, and also the classic ReDoS trap: (a+)+$ against a long string of as explodes exponentially.

Linear-time engines — RE2, Go, Rust. They compile patterns to automata and guarantee O(n) matching. The price: no backreferences and no lookarounds, at all. Go and Rust reject such patterns at compile time — which is exactly the moment your Python pattern "stops working".

PCRE2 is the superset most people mean by "regex": recursion (?R), conditionals (?(1)...), match-start reset \\K, possessive quantifiers. If an answer you found online uses (?R), it lives in PCRE/Perl territory and will not run anywhere else on this list.

The feature matrix

Feature PCRE2 RE2 JavaScript Python Go Java .NET Rust
Lookahead yes no yes yes no yes yes no
Lookbehind variable length no variable (ES2018) fixed width no bounded variable no
Backreferences yes no yes yes no yes yes no
Named groups (?<n>...) (?P<n>...) (?<n>...) (?P<n>...) (?P<n>...) (?<n>...) (?<n>...) (?P<n>...)
Atomic groups yes no no 3.11+ no yes .NET 7+ no
Possessive quantifiers yes no no 3.11+ no yes no no
Recursion (?R) yes no no no no no no* no
Unicode \\p{L} yes yes with u flag yes yes yes yes yes

*.NET has balancing groups, which cover some of the same ground as recursion.

Five breakages you will actually hit

  1. Lookbehind width. JavaScript (since ES2018) and .NET accept variable-length lookbehind like (?<=\\d+\\.?\\d*). Python requires fixed width. Java allows bounded width. Go and Rust reject lookbehind entirely — pattern won't compile.
  2. Backreferences. (\\w+)@\\1 style patterns work in five engines and are rejected by RE2, Go, and Rust at compile time.
  3. Named group syntax. Guess between (?<name>...) and (?P<name>...) and you have a 50% chance of a syntax error per engine.
  4. Possessive quantifiers. x*+ runs in PCRE2, Java, and Python 3.11+, and throws a syntax error in JavaScript and .NET.
  5. The ReDoS cliff. A pattern that is merely slow on PCRE2/JS/Python is impossible to blow up on RE2/Go/Rust. Moving user-supplied patterns to a linear-time engine is a security fix, not just a port.

Test before you ship

The cheapest insurance is running the pattern through every engine you actually target:

  • RegexTester.dev runs real WebAssembly builds of all eight engines side by side — same pattern, eight result columns, so flavor differences become visible instead of hypothetical. It also benchmarks the same pattern across engines with P95/P99 timings.
  • Its compatibility checker flags the exact constructs in your pattern that behave differently across flavors before they reach production.
  • Per-flavor testers if you only care about one target: PCRE2, JavaScript, Python, Go, Rust.

Takeaways

  • "Regex" is really eight languages wearing one syntax. Budget for translation, not copy-paste.
  • The dangerous constructs are not the ones that fail loudly — a compile error is a good error. The dangerous ones compile everywhere and mean slightly different things (lookbehind width, Unicode case folding).
  • Before shipping a pattern to a second language, run it through all engines once. It takes a minute and catches the whole class of portability bugs.

What is the worst regex portability bug you have shipped? I am collecting examples.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí