skip to content

RE2 Semantics and Limits

regexp runs an RE2 automaton, so a pattern can never blow up exponentially, but lookarounds and backreferences simply do not exist. Interviewers ask what you do instead.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

Why does Go's regexp package reject lookaheads and backreferences?

level: juniorimportance: must knowfreq 60%

answer

  1. the package is not a backtracker
  2. one guarantee shapes the whole syntax
  3. the search simulates all positions at once
  4. a backreference needs unbounded memory
  5. the pattern fails at compile time, not match time

basics

~20 s

Go's regexp implements RE2 semantics: it simulates an automaton over the input instead of backtracking, so matching time stays bounded by input length times pattern size. Lookaheads and backreferences cannot be expressed in that model, so such patterns fail to compile.

solid answer

~40 s

Go's `regexp` package implements RE2 syntax and semantics, and its promise is that a match costs time proportional to the length of the input times the size of the compiled pattern — never exponential. It gets that by simulating all possible positions in the pattern at once (a Thompson NFA simulation) rather than backtracking through choice points. A backreference would require remembering and re-matching what a group captured, and a lookaround assertion would require re-running a sub-pattern at the current position; neither fits the simulation, so the pattern parser refuses them. `regexp.MustCompile("foo(?=bar)")` panics with a parse error reading "invalid or unsupported Perl syntax" and naming the offending fragment, and a `\1` backreference is rejected as an invalid escape sequence. The rejection is always at compile time, never at match time.

code

go · 5 lines
go
// panics: error parsing regexp: invalid or unsupported Perl syntax: `(?=`
re := regexp.MustCompile(`foo(?=bar)`)

// returns a non-nil error: `\1` is not a valid escape in this syntax
_, err := regexp.Compile(`(\w+) \1`)

go deeper

for a junior

Be able to name the two big missing features, lookaround and backreferences, and say that a pattern using them does not compile at all. Knowing that the reason is a linear-time guarantee is enough at this level.

for a middle

Explain the mechanism: the engine advances a set of possible pattern positions per input rune instead of backtracking, which bounds the work at input length times pattern size and rules out constructs that need a stack or remembered captures.

for a senior

Show you can port real patterns. Talk about two-stage matching for negative lookaheads, about validating a whole rule set at build time because failures are compile-time, and about the fact that linear worst case is not the same as fast.

for a principal

Own the consequence for the codebase: whether the team accepts the RE2 dialect as a constraint on everyone who writes rules, and what the alternative — an outside engine with an unbounded worst case — would cost in review, operations and dependency governance.

Go's `regexp` package is not a backtracking engine, and almost everything surprising about it follows from that one fact. ## The promise The package implements the syntax and semantics of RE2 (the grammar lives in `regexp/syntax`). Its guarantee is that a search runs in time proportional to `len(input)` multiplied by the size of the compiled pattern, for every pattern and every input. There is no pattern you can write that makes it take exponential time. ## How a backtracking engine differs PCRE, Perl, Java's `java.util.regex`, Python's `re` and JavaScript's `RegExp` compile a pattern into a small program that walks one path through the pattern and, when that path fails, returns to the most recent choice point and tries the next alternative. That recursive search is exactly what makes the richer features possible: the engine has a stack, it remembers the text that group 1 captured, and it can re-run a sub-pattern at the current position to evaluate an assertion. The price is that some patterns explore an enormous number of paths, so worst-case running time can grow exponentially with input length. ## How Go's engine works Go parses the pattern into a syntax tree, compiles it to a program of instructions, and then *simulates* that program: it keeps the set of instructions the match could currently be at and advances every one of them by one input rune at a time. Because the live set can never be larger than the program, each input character costs at most O(pattern) work, and the whole search is O(len(input) x len(pattern)). Go also has a one-pass fast path for patterns with no ambiguity and a bounded-backtracking path for small inputs, but the latter memoizes the (instruction, position) pairs it has already visited, so the linear bound still holds. ## Why the two features do not fit A **backreference** (`(\w+) \1`, "the same word twice") makes the pattern describe a non-regular language: the machine would have to carry an unbounded memory of arbitrary captured text and compare it later. No finite automaton can do that, so RE2 — and therefore Go — leaves it out of the syntax entirely. **Lookahead** (`(?=re)`, `(?!re)`) and **lookbehind** (`(?<=re)`) are zero-width assertions: they ask "would this sub-pattern match here?" without consuming input. That is expressible in theory, but not inside a single left-to-right simulation of one program, so RE2 does not implement it. Related Perl extras built on backtracking — atomic groups, possessive quantifiers, recursion, `\K` — are absent for the same reason. ## What the failure looks like Parsing fails, so the error arrives when you build the `*regexp.Regexp`, not when you search: - `foo(?=bar)` produces `error parsing regexp: invalid or unsupported Perl syntax: (?=` - `(\w+) \1` is rejected as an invalid escape sequence, because `\1` is not a valid escape in this syntax. `regexp.MustCompile` turns that failure into a panic; `regexp.Compile` returns it as an error. Either way the whole rule set can be validated at start-up or in a test, which is why porting a rule set from another engine fails loudly and all at once rather than silently misbehaving in production. ## What you do still get Everything that fits an automaton: alternation, greedy and non-greedy repetition, character classes including Unicode classes such as `\p{Greek}`, the word boundary `\b`, anchors, capture groups and named groups, and the inline flags `(?i)`, `(?m)`, `(?s)`, `(?U)`. Patterns and input are treated as UTF-8, and `.` matches a whole rune rather than a byte. ## Working around the gap Most negative lookaheads in real rule sets translate into two matches in Go code: match the broad pattern with one `*regexp.Regexp`, then reject the hit with a second one. "Starts with `/api/` but not `/api/health`" is two patterns and a boolean, not one regexp. Backreference rules usually cannot be translated at all and become a few lines of Go — extract with a capture group, then compare the strings yourself. ## The nuance people get backwards A linear worst case does not mean "faster". On short inputs with simple patterns a backtracking engine often beats Go's `regexp`, because the simulation pays per-character overhead the backtracker skips, and Go's implementation does not include the lazy DFA that the C++ RE2 has. Go trades some typical-case speed for a worst case that cannot blow up.

  • Does the linear-time guarantee mean Go's regexp is faster than a backtracking engine?
    No. The guarantee is about the worst case, not the typical case. For simple patterns on short strings a backtracking engine frequently wins, because Go's simulation advances a set of live states on every input rune and pays overhead a single-path search avoids. What Go buys you is that no pattern-and-input combination can suddenly cost seconds instead of microseconds.
  • How would you express "match /api/ paths except /api/health" without a negative lookahead?
    With two patterns and ordinary Go code: compile `^/api/` and `^/api/health(/|$)`, then treat a path as a hit when the first matches and the second does not. Two-stage matching covers most negative lookaheads in ported rule sets, and it is easier to read and test than the single pattern was.
  • When does an unsupported construct surface — at compile time or at match time?
    Always at compile time. `regexp/syntax` rejects the pattern while parsing, so `regexp.Compile` returns an error and `regexp.MustCompile` panics. Nothing about an unsupported construct can survive to a `MatchString` call, which means compiling every pattern once at start-up or in a test finds all of them at once.

A backtracker is a person exploring a maze depth-first, retracing steps on every dead end; Go's engine floods every corridor at once, one step at a time. The flood always finishes on schedule, but it cannot remember where it has been in the way the walker can.

saying these in an interview costs you the question

  • Says Go supports lookahead but the syntax is different
  • Claims backreferences work if you use MustCompile
  • Thinks the unsupported pattern fails on first match, not at compile
  • Says RE2 semantics make Go's regexp faster than every other engine
  • Believes Go compiles patterns to a DFA up front for constant-time matching
open as a page

In Go's regexp package, how do you turn on case-insensitive or dot-matches-newline matching?

level: middleimportance: should knowfreq 45%

basics

~20 s

Put the flag inside the pattern text: (?i) for case-insensitive, (?s) to let . match a newline. Go's regexp has no flags argument, so (?i), (?m), (?s) and (?U) written in the pattern are the only way to set modes.

open as a page

Go's regexp panics on a rule set ported from a PCRE engine at proxy start-up — how do you triage which rules are unportable and rewrite them?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Stop the crash-loop, then compile every rule in a test so failures surface in CI rather than at start-up. Group the parse errors by construct, rewrite lookarounds as two-stage matches in Go, and add traffic fixtures for rules that still compile.

open as a page

Go's regexp cannot express your rules' lookaheads — how do you decide between rewriting them and adopting a backtracking regex dependency?

level: principalimportance: should knowfreq 26%

basics

~20 s

Measure first: compile the whole rule set and count how many rules truly need the missing constructs. Rewriting a handful beats owning an engine with an unbounded worst case, a dependency to govern, and a rule dialect that is hard to walk back.

open as a page

In Go, why does regexp.MustCompile(`a|ab`).FindString("ab") return "a" rather than "ab"?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Go's regexp uses leftmost-first (Perl-style) semantics: among matches starting at the earliest position, it picks the one a backtracking search would find first, so the alternative a wins. Compile with regexp.MustCompilePOSIX, or call Longest, to get leftmost-longest instead.

open as a page