skip to content

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

level: middleimportance: nice to knowfreq 30%

answer

  1. two different rules can pick the winner
  2. alternation is ordered, not sorted by length
  3. the default reproduces what a backtracker would find
  4. one constructor and one method switch the rule
  5. the switching method mutates the compiled value

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.

solid answer

~40 s

By default Go's `regexp` returns the match that begins as early as possible in the input and, among those, the one a backtracking search starting at the left of the pattern would have found — so alternation is ordered, and `a` in `a|ab` wins at position 0 even though `ab` is longer. Two things change it. `regexp.CompilePOSIX` and `regexp.MustCompilePOSIX` restrict the pattern to POSIX ERE (egrep) syntax and switch the semantics to leftmost-longest, so `a|ab` returns `ab`. Alternatively `(*Regexp).Longest()` makes future searches on an already-compiled pattern prefer the leftmost-longest match; it mutates the `Regexp`, so call it right after compiling and never concurrently with a search. The practical rule: order your alternatives longest-first if you rely on the default.

code

go · 5 lines
go
re := regexp.MustCompile(`a|ab`)
fmt.Println(re.FindString("ab")) // a  — leftmost-first

posix := regexp.MustCompilePOSIX(`a|ab`)
fmt.Println(posix.FindString("ab")) // ab — leftmost-longest

go deeper

for a junior

Take away one habit: in a Go alternation the first alternative that matches at the earliest position wins, so list longer keywords before their shorter prefixes.

for a middle

Name both rules and both switches: leftmost-first by default, leftmost-longest via the POSIX compile functions or the Longest method, and explain why the default makes alternation order significant.

for a senior

Treat this as a silent porting hazard. Patterns from POSIX tools compile fine and capture different text, so argue for fixture tests over real inputs, and for calling Longest only at construction time because it mutates a value other goroutines share.

for a principal

Set the house rule: which selection semantics the codebase standardises on, whether rule authors may assume longest-match, and how a mode chosen far from the pattern is documented so a future reader does not undo it.

Two different rules can decide which match a regexp engine reports, and Go can do both. ## Leftmost-first, the default Go's `regexp` documents its default as: the match begins as early as possible in the input (leftmost), and among the matches that start there, the engine returns the one a backtracking search that started at the beginning of the pattern would have found first. That second half is what makes alternation *ordered*. In `a|ab`, the left alternative is tried first; at input position 0 of `"ab"` the alternative `a` succeeds, so the search is done and `FindString` returns `"a"`. The longer alternative is never preferred, because preference is by position in the pattern, not by length. This is also why non-greedy operators mean something: `a+?` and `a+` start at the same place but express different preferences within the same alternation order. Perl, PCRE, Java, Python and JavaScript all use this rule, so a pattern ported from any of them keeps its selection behaviour in Go, even though Go's engine is not a backtracker. The simulation is built to reproduce the *result* a backtracker would give, not its cost. ## Leftmost-longest, the POSIX rule POSIX specifies a different rule: among matches starting at the earliest position, take the longest one, regardless of how the alternatives are ordered. Under that rule `a|ab` against `"ab"` yields `"ab"`. Go exposes it two ways. **`regexp.CompilePOSIX` / `regexp.MustCompilePOSIX`** restrict the pattern to POSIX ERE (egrep) syntax and change the match semantics to leftmost-longest. The syntax restriction is part of the deal, so a Perl-flavoured pattern may no longer compile. **`(*Regexp).Longest()`** switches an already-compiled `Regexp` to prefer leftmost-longest for future searches, without changing which syntax was accepted. It modifies the `Regexp` in place and, per its documentation, may not be called concurrently with any other method — so the safe shape is to compile the pattern into a package-level value and call `Longest` immediately, before any goroutine can search with it. Calling it halfway through a service's life, on a pattern other goroutines are already using, is a data race. Because leftmost-longest ignores the ordering of alternatives, preference operators lose their meaning under it: there is no way to ask for a shorter match when the rule is "take the longest". ## When it actually bites The classic bug is a token or keyword pattern written as an alternation where a short alternative is a prefix of a long one: `int|integer`, `GET|GETALL`, `v1|v10`. With the default rule the short one always wins, and the extra text is left in the input for the next match, so a scanner silently splits tokens in the wrong place. The fix in the default mode is ordering — write `integer|int` — or anchoring with `\b` so the short alternative cannot match a prefix of the long one. The second place it bites is porting. A rule set written for `egrep`, `awk` or another POSIX tool assumes longest-match, and dropping those patterns into Go's default compile silently changes which text a rule captures. The patterns compile, the tests may even pass on simple inputs, and the difference shows up on the one input where a short alternative is a prefix of a long one. This is a *semantic* skew rather than a syntax error, so no compile-time check will catch it; only fixtures drawn from real inputs will. ## Which to choose Keep the default unless you have a specific reason. It is what almost every other engine does, so patterns move in and out of Go without surprises, and it lets non-greedy operators express intent. Reach for POSIX mode when you are implementing something that must follow the POSIX rule — reproducing a tool's behaviour, or honouring a spec written against it — and make the choice explicit and commented, because a reader who sees only `Longest()` a hundred lines from the pattern will not connect the two. One more detail worth knowing: the choice affects which match is *reported*, not whether a match exists. `MatchString` returns the same boolean either way. It changes the text and the offsets you get back, which is exactly why it shows up as a data bug rather than a matching failure.

  • Without switching modes, how do you make `a|ab` match the longer text?
    Reorder the alternatives: write `ab|a`, so the longer alternative is tried first at the same starting position. In real patterns that means listing keywords longest-first, or anchoring with `\b` so a short alternative cannot match a prefix of a longer one. Ordering is the tool the default rule gives you.
  • What is unsafe about calling Longest on a *regexp.Regexp?
    It modifies the Regexp in place and is documented as not safe to call concurrently with any other method. A compiled Regexp is otherwise usable from many goroutines at once, so flipping the mode while requests are searching is a data race. Call it immediately after compiling, before the value is shared.
  • Does switching to leftmost-longest change whether a pattern matches at all?
    No. Both rules search the same language, so a boolean match test gives the same answer either way. What changes is which text and which offsets are reported when several matches start at the same earliest position — which is why the difference shows up as wrong extracted data rather than as a missed match.

saying these in an interview costs you the question

  • Says the engine always returns the longest possible match
  • Thinks alternation order in a Go pattern is irrelevant
  • Believes POSIX mode only changes which syntax is allowed
  • Assumes Longest can be flipped safely on a shared Regexp
  • Claims the selection rule decides whether a match exists