Regular Expressions Module 8 – Flavours compared and regex in each language
Regex in Go, RE2 and re2js
How Go's regexp keeps every match linear in the text, what RE2 will not compile, which method returns what, the template that drops text, and RE2 in browsers.
What you will learn
- Explain why Go's regexp runs in time linear in the input, and name the constructs it refuses in exchange
- Use the find, replace, split and name methods of Go's regexp, and read the byte offsets that FindAllStringSubmatchIndex returns
- Write Go replacement templates with braces, $$ or ReplaceAllLiteralString so that no text is lost
- Predict where Go's empty-match rule, CompilePOSIX and the (?U) flag change results against JavaScript and Python
- Compare a pattern's outputs in Go and in re2js before relying on it in a browser
Before you start
What you’ll learn · Before you start
What you will learn
- Explain why Go's regexp runs in time linear in the input, and name the constructs it refuses in exchange
- Use the find, replace, split and name methods of Go's regexp, and read the byte offsets that FindAllStringSubmatchIndex returns
- Write Go replacement templates with braces, $$ or ReplaceAllLiteralString so that no text is lost
- Predict where Go's empty-match rule, CompilePOSIX and the (?U) flag change results against JavaScript and Python
- Compare a pattern's outputs in Go and in re2js before relying on it in a browser
Before you start
On this page
Ask Go’s regexp package whether ^(a+)+$ matches a line of 100,000 letters a followed by !, and it answers
false within a limit it knows before it starts: the size of the compiled pattern times the length of the line. The
package documentation promises that every match runs in time linear in the size of the input, a property it says most
open source engines do not guarantee. The price is a list of constructs Go will not compile, and the compiler prints
that list itself.
That trade shapes the whole package. Here you read the refusals and the reason behind them, choose the method that returns what you need, avoid the template rule that deletes text without an error, predict Go’s own rule for empty matches, use the two switches that change which text matches, and see what changes when the same patterns run in a browser.
Linear time, paid for with the features RE2 refuses
Go’s regexp accepts the syntax of RE2: A regular-expression library and syntax from Google whose primary guarantees include match time linear in the input, which is why it leaves out backreferences and lookaround. Go's regexp package accepts its syntax., a library from Google whose README names linear match
time as one of its primary guarantees, and checks every pattern when it compiles it. Give the compiler one small pattern for each construct a Perl-style engine offers,
and its answers are the list of what the syntax leaves out:
// Go's regexp package checks every pattern when it compiles it, and refuses the constructs that RE2 leaves out.
// Each line shows a construct, a small pattern that uses it and what regexp.Compile answered. Every refusal starts
// with "error parsing regexp: ", so the program prints that prefix once and drops it from the lines.
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
probes := []struct{ feature, pattern string }{
{"backreference", `(a)\1`},
{"named backreference", `\k<n>`},
{"lookahead", `(?=a)`},
{"negative lookahead", `(?!a)`},
{"lookbehind", `(?<=a)`},
{"negative lookbehind", `(?<!a)`},
{"possessive", `a++`},
{"atomic group", `(?>a)`},
{"end or final newline", `a\Z`},
{"grapheme cluster", `\X`},
{"free-spacing mode", `(?x)a`},
{"count above 1000", `a{1001}`},
{"end of last match", `\Ga`},
{"named group (?<n>)", `(?<n>a)`},
{"named group (?P<n>)", `(?P<n>a)`},
}
const prefix = "error parsing regexp: "
fmt.Println("Every refusal below starts with:", strings.TrimSpace(prefix))
for _, p := range probes {
answer := "compiles"
if _, err := regexp.Compile(p.pattern); err != nil {
answer = strings.TrimPrefix(err.Error(), prefix)
}
fmt.Printf("%-21s %-8s %s\n", p.feature, p.pattern, answer)
}
} Output
Every refusal below starts with: error parsing regexp:
backreference (a)\1 invalid escape sequence: `\1`
named backreference \k<n> invalid escape sequence: `\k`
lookahead (?=a) invalid or unsupported Perl syntax: `(?=`
negative lookahead (?!a) invalid or unsupported Perl syntax: `(?!`
lookbehind (?<=a) invalid named capture: `(?<=a)`
negative lookbehind (?<!a) invalid named capture: `(?<!a)`
possessive a++ invalid nested repetition operator: `++`
atomic group (?>a) invalid or unsupported Perl syntax: `(?>`
end or final newline a\Z invalid escape sequence: `\Z`
grapheme cluster \X invalid escape sequence: `\X`
free-spacing mode (?x)a invalid or unsupported Perl syntax: `(?x`
count above 1000 a{1001} invalid repeat count: `{1001}`
end of last match \Ga invalid escape sequence: `\G`
named group (?<n>) (?<n>a) compiles
named group (?P<n>) (?P<n>a) compiles
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run refusals.go
Thirteen of the fifteen patterns fail, and only the two spellings of a named group compile. RE2’s syntax page marks
the refused forms NOT SUPPORTED, from backreferences, lookahead and lookbehind to possessive quantifiers, atomic
groups, \Z, \G and \X, and its table of flags holds only i, m, s and U, so (?x) fails as well. The repetition
limit comes from regexp/syntax, which rejects any counted form whose minimum or maximum goes above 1,000. Because the
refusal comes from the compiler, a program learns of it when it builds the pattern, never halfway through a file of
input.
Hunting for a naming rule after invalid named capture
Wrong: reading invalid named capture: (?<=a) as a complaint about a group’s name and searching Go’s naming rules.
To Go’s parser, (?< opens a named group, so it takes a lookbehind for a group whose name begins with = or !.
Right: read the message as “this pattern has a lookbehind” and rewrite that part with a capture group, or with a
check in code.
Why give up so much? A backtracking engine follows one path through the pattern at a time and returns to a saved
choice when that path fails, as how an engine finds a match shows step by
step. Go follows every path at once. It compiles the pattern into a small program and keeps the set of instructions
that are still alive at the current character; each character is read once and moves the whole set forward. Russ
Cox’s article traces this multiple-state method to Ken Thompson’s 1968 paper and explains why it is linear: the
simulation tracks which states are reachable, not the paths that reached them, so the set never holds more entries
than the program has instructions. The same article notes that the best known ways to support backreferences need
exponential search in the worst case. That is the price of
linear-time matching: A guarantee that, for a fixed pattern, the time a match takes grows at most in proportion to the length of the text, whatever the text holds. Go's regexp gives it by following every path of the pattern at once.. Step through (a|ab)c on abc, the case the matching
lesson solves by backing up, and watch both alternatives move forward together:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Both paths start together. The pattern (a|ab)c offers two ways to begin. At position 0 the engine starts both at once: path 1 wants a and then c, path 2 wants a, b and then c. Nothing is saved for later, because no path waits its turn.
- Read a. The engine reads the a at position 0 once and hands it to both paths. Both accept it: path 1 now needs c, path 2 needs b.
- Read b. The b at position 1 ends path 1, which needed c. It is dropped for good: no later step returns to it. Path 2 takes the b and now needs c.
- Read c. The c at position 2 completes path 2, so the whole pattern has matched: the match is abc, from position 0 up to position 3.
- Each character read once. Each of the three characters was read once, and no more than two paths were alive at any step. A backtracking engine reads a and b twice on this text: once for the path that fails, and again after it backs up to try ab.
Path 1 died at the b and nothing came back for it. On a long text the set can hold many paths, but never more than
the program has instructions, so the work for each character has a ceiling set by the pattern alone. To report
groups as well, Go’s matcher gives each live path a thread that carries its own group offsets (the package source,
exec.go), the design Russ Cox’s second article calls a Pike VM: A way to run a compiled pattern that keeps at most one thread per instruction, each with its own group offsets, and moves them all forward one character at a time. Rob Pike first used it in the sam editor., after Rob Pike’s sam editor.
For small patterns on short texts Go uses a backtracker instead, which marks every pair of an instruction and a
position it has visited, so it never does the same work twice (backtrack.go).
The ceiling can be printed. The program below runs three pattern shapes that the module on performance and security returns to, each on a long text that does not match, and prints the answer with its bound instead of a time: the instructions of the compiled program times the positions of the text.
// Three pattern shapes that are slow in backtracking engines, run by Go on long texts that do not match. The program
// prints each answer and the bound that makes it come back: the compiled program's instructions times the positions
// of the text, because the engine handles each pair of an instruction and a position at most once. No timing is
// printed: the bound, not a stopwatch, is the point.
package main
import (
"fmt"
"regexp"
"regexp/syntax"
"strings"
)
// instructions compiles a pattern the way regexp.Compile does (parse, simplify, compile) and counts the program.
func instructions(pattern string) int {
tree, err := syntax.Parse(pattern, syntax.Perl)
if err != nil {
panic(err)
}
prog, err := syntax.Compile(tree.Simplify())
if err != nil {
panic(err)
}
return len(prog.Inst)
}
func main() {
cases := []struct{ pattern, text string }{
{`^(a+)+$`, strings.Repeat("a", 100000) + "!"},
{`(a|aa)*b`, strings.Repeat("a", 100000)},
{`\s+$`, strings.Repeat(" ", 40000) + "x"},
}
for _, c := range cases {
matched := regexp.MustCompile(c.pattern).MatchString(c.text)
n, positions := instructions(c.pattern), len(c.text)+1
fmt.Printf("%-9s on %d characters: match %v\n", c.pattern, len(c.text), matched)
fmt.Printf("%-9s %d instructions x %d positions = at most %d pairs\n", "", n, positions, n*positions)
}
} Output
^(a+)+$ on 100001 characters: match false
9 instructions x 100002 positions = at most 900018 pairs
(a|aa)*b on 100000 characters: match false
10 instructions x 100001 positions = at most 1000010 pairs
\s+$ on 40001 characters: match false
5 instructions x 40002 positions = at most 200010 pairs
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run linear.go
^(a+)+$ compiles to 9 instructions, and 100,001 characters give 100,002 positions, so the engine has at most 900,018
pairs of an instruction and a position to handle before it can answer false. (a|aa)*b compiles to 10 instructions
and \s+$ to 5. Double the text and the bound doubles too, give or take one position. Why such shapes can take a backtracking engine far longer, and
how to defuse them there, belongs to the performance module; in Go no pattern can lift the ceiling above its program’s size
times the text’s length. Go is not the only engine that makes this promise, and
Regex in Java, .NET, Ruby and Rust shows which engines on those
platforms do.
Key idea
Go bounds every match by the compiled pattern’s size times the text’s length because it follows all paths at once; the constructs that need more than a set of positions, backreferences and lookaround first, are refused when the pattern compiles.
Checkpoint
Check: Linear time and the constructs it costs
Pick the method by what you want back
Most of the package is one question asked in different shapes. The finding methods are named by a rule: Find for the
first match or FindAll for every match, then String when the text is a string rather than a byte slice,
Submatch to get the groups as well, and Index to get positions instead of text. With the calls that replace, split
and name, this table covers everyday work on strings:
| You want back | Call | It returns |
|---|---|---|
| Whether the text matches anywhere | MatchString(s) |
true or false |
| The first match | FindString(s) |
its text; "" for no match and for an empty match alike |
| The first match and its groups | FindStringSubmatch(s) |
the match, then each group’s text; nil for no match |
| Every match with each group’s position | FindAllStringSubmatchIndex(s, -1) |
one list of byte offsets per match |
| A copy with groups put back | ReplaceAllString(src, repl) |
the copy, with repl read as a template |
| A copy with computed text | ReplaceAllStringFunc(src, f) |
the copy, with f called on each match |
| A copy with text exactly as given | ReplaceAllLiteralString(src, repl) |
the copy, with repl inserted unread |
| The pieces between matches | Split(s, -1) |
a list of strings |
| A group’s number from its name | SubexpIndex(name) |
the number, or -1 when no group has that name |
A count of -1 asks for every match, and a count of n for at most n. The return value that needs reading is the index
list. The program below reads three invented lines of an access log with one pattern of
named group: A capturing group that carries a name as well as a number, so the match reads back by name instead of by slot: (?<year>\d{4}) in JavaScript, PCRE2, Go, PHP, Java and .NET; (?P<year>\d{4}) in Python, Go, PHP and PCRE2. More in “Named groups across regex flavours”, written in both spellings Go accepts, (?P<time>…) and (?<method>…),
side by side. It prints SubexpNames, then the list FindAllStringSubmatchIndex gives for each line, and slices two
fields out of each line by the group numbers that SubexpIndex looked up:
// A short access log read with one pattern of named groups (both spellings Go accepts). For each match,
// FindAllStringSubmatchIndex returns one flat list of byte offsets: start and end of the whole match, then start
// and end of every group in order; -1 -1 marks a group that took no part in the match.
package main
import (
"fmt"
"regexp"
"unicode/utf8"
)
const accessLog = `09:14:03 GET /orders/42 200 31ms user=asha
09:14:04 POST /cart 500 812ms
09:14:05 GET /menu/café 200 9ms user=ravi`
func main() {
re := regexp.MustCompile(`(?m)^(?P<time>\S+) (?<method>[A-Z]+) (?<path>\S+) (?<status>\d{3}) (?<ms>\d+)ms(?: user=(?<user>\w+))?$`)
fmt.Printf("names %q\n", re.SubexpNames())
status, user := re.SubexpIndex("status"), re.SubexpIndex("user")
fmt.Println("status is group", status, "and user is group", user)
for _, m := range re.FindAllStringSubmatchIndex(accessLog, -1) {
fmt.Println(m)
start, end := m[2*status], m[2*status+1]
who := "took no part"
if m[2*user] >= 0 {
who = accessLog[m[2*user]:m[2*user+1]]
}
fmt.Printf(" status %s at bytes %d to %d (character %d), user %s\n",
accessLog[start:end], start, end, utf8.RuneCountInString(accessLog[:start]), who)
}
} Output
names ["" "time" "method" "path" "status" "ms" "user"] status is group 4 and user is group 6 [0 42 0 8 9 12 13 23 24 27 28 30 38 42] status 200 at bytes 24 to 27 (character 24), user asha [43 72 43 51 52 56 57 62 63 66 67 70 -1 -1] status 500 at bytes 63 to 66 (character 63), user took no part [73 115 73 81 82 85 86 97 98 101 102 103 111 115] status 200 at bytes 98 to 101 (character 97), user ravi
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run logscan.go
Read the first list two numbers at a time. 0 42 is the whole match, from byte 0 up to byte 42; then come the groups
in the order their parentheses open: time at 0 to 8, method at 9 to 12, path at 13 to 23 and status at 24 to
27. Group k always sits at places 2k and 2k + 1 of the list, which is how the program finds status, group 4, at places
8 and 9. The second line ends in -1 -1: its optional user group took no part in the match, and the documentation
gives a negative index exactly that meaning. The third line shows that the numbers count bytes, not characters. Its
path holds café, and é takes two bytes in UTF-8, so the status begins at byte 98 but at character 97.
Slice with the offsets, count characters apart
Use the offsets to slice the same Go string, as line[m[8]:m[9]], which is always right. When another program needs
character positions, count the characters before the offset with utf8.RuneCountInString, as the run does for each
status.
FindString has one trap of its own in the table: its "" can mean that nothing matched or that the pattern matched
nothing, and the documentation sends you to FindStringIndex or FindStringSubmatch to tell the two apart.
Checkpoint
Check: Go's regexp methods and their index lists
Braces keep a template from swallowing text
A masking rule keeps the first two digits of an order number and hides the other four, so ORD-123456 should become
ORD-12xxxx. The pattern (ORD-\d{2})\d{4} captures the part to keep, and the
replacement string: The new text given to a replace function. It has its own escapes, so a dollar sign or a backslash in it may name part of the match instead of standing for itself. More in “Writing patterns in code: strings and escaping” puts group 1 back with four letters x after it. Here is the
template $1xxxx in Go and in Python, beside the two other spellings people carry between languages:
Go 1.27.1 · templates/templates.go
// A masking rule: keep the first two digits of an order number and hide the other four. Go reads only $ in a
// template, and in $1xxxx it takes the name as far as letters, digits and underscores go: it asks for a group named
// 1xxxx, which the pattern does not have, and writes nothing in its place.
package main
import (
"fmt"
"regexp"
)
func main() {
line := "order ORD-123456 shipped"
re := regexp.MustCompile(`(ORD-\d{2})\d{4}`)
for _, template := range []string{`$1xxxx`, `${1}xxxx`, `\1xxxx`} {
fmt.Printf("%-16s \"%s\"\n", template, re.ReplaceAllString(line, template))
}
fmt.Printf("%-16s \"%s\"\n", "literal $1xxxx", re.ReplaceAllLiteralString(line, "$1xxxx"))
fmt.Printf("%-16s \"%s\"\n", "$$1xxxx", re.ReplaceAllString(line, "$$1xxxx"))
hide := func(m string) string { return m[:len(m)-4] + "xxxx" }
fmt.Printf("%-16s \"%s\"\n", "a function", re.ReplaceAllStringFunc(line, hide))
// The same rule in two letters: group 1 is a, and the text is ab.
a := regexp.MustCompile(`(a)`)
fmt.Printf("%q %q %q\n", a.ReplaceAllString("ab", "$1x"), a.ReplaceAllString("ab", "${1}x"), a.ReplaceAllLiteralString("ab", "$1x"))
} Output
$1xxxx "order shipped"
${1}xxxx "order ORD-12xxxx shipped"
\1xxxx "order \1xxxx shipped"
literal $1xxxx "order $1xxxx shipped"
$$1xxxx "order $1xxxx shipped"
a function "order ORD-12xxxx shipped"
"b" "axb" "$1xb"
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run templates.go
Python 3.14.8 · templates/templates.py
"""The masking rule in Python: its templates name a group with a backslash, \\1 or \\g<1>, so a dollar sign and a
pair of braces are plain text there and are written into the result as they are."""
import re
line = "order ORD-123456 shipped"
pattern = re.compile(r"(ORD-\d{2})\d{4}")
for template in ("$1xxxx", "${1}xxxx", r"\1xxxx", r"\g<1>xxxx"):
print(f'{template:<16} "{pattern.sub(template, line)}"')
result = pattern.sub(lambda m: m[1] + "xxxx", line)
print(f'{"a function":<16} "{result}"') Output
$1xxxx "order $1xxxx shipped"
${1}xxxx "order ${1}xxxx shipped"
\1xxxx "order ORD-12xxxx shipped"
\g<1>xxxx "order ORD-12xxxx shipped"
a function "order ORD-12xxxx shipped"
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 templates.py
Go’s first line is the one to remember: the whole order number is gone, two spaces stand between order and
shipped, and nothing reported an error. In a template Go reads $ and then a name, and the name runs as far as letters, digits and underscores go, so $1xxxx
asks for a group named 1xxxx; the documentation of Expand uses $1x as its own example. The pattern has no such
group, and a missing group becomes empty text. Braces end the name where you mean it: ${1}xxxx keeps ORD-12. A
backslash means nothing to Go, so \1xxxx comes out as written. Python is the other way round: it reads only the
backslash forms, \1 and \g<1>, and copies the dollar forms as text. JavaScript reads $1 followed by letters as
group 1 and then text, and copies ${1} as it stands:
// The masking rule in JavaScript: a replacement reads $1 as group 1 and the letters after it as text. It has no ${1}
// form and gives a backslash no meaning, so those two spellings are written into the result as they are.
const line = 'order ORD-123456 shipped';
const re = /(ORD-\d{2})\d{4}/g;
for (const template of ['$1xxxx', '${1}xxxx', '\\1xxxx', '$$1xxxx']) {
console.log(template.padEnd(16), `"${line.replace(re, template)}"`);
}
console.log('a function'.padEnd(16), `"${line.replace(re, (match, first) => first + 'xxxx')}"`); Output
$1xxxx "order ORD-12xxxx shipped"
${1}xxxx "order ${1}xxxx shipped"
\1xxxx "order \1xxxx shipped"
$$1xxxx "order $1xxxx shipped"
a function "order ORD-12xxxx shipped"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node templates.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Press Run, then Edit: turn the group into (?<keep>ORD-\d{2}), add the template $<keep>xxxx to the list and run it
again. JavaScript names a group in a replacement with angle brackets, where Go writes ${keep} and Python
\g<keep>; the porting lesson sets the spellings of every engine side
by side. Lines four to six of the Go run are its other ways out: ReplaceAllLiteralString inserts its text unread,
$$1xxxx writes a single dollar sign, and ReplaceAllStringFunc hands each match to a function, where no template
rule can reach it. The final line repeats the porting lesson’s two-letter case: (a) with $1x on ab gives b,
and with ${1}x gives axb.
Key idea
In a Go template, put every group reference in braces, ${1} or ${name}, write $$ for a dollar sign, and send
text that is not a template through ReplaceAllLiteralString and computed text through ReplaceAllStringFunc.
Checkpoint
Check: Replacement templates in Go
Go skips the empty match right after a match
Replace every match of a* in baac with a dash. JavaScript and Python return -b--c-, and Go returns -b-c-. The
star can match nothing, so a search finds empty match: A match of length zero: the pattern succeeds at a position without taking any character, as x* does where no x stands. Global searches, splits and replacements report such matches, each engine by its own rules. More in “Repetition with *, +, ? and {m,n}” besides aa, and the engines
disagree about one of them. Step through the four places a search for a* stops in baac:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- An empty match at 0. Position 0 holds b, which is not a. The star allows zero repetitions, so a* matches there anyway, with nothing: an empty match from 0 to 0. Both engines keep it.
- aa from 1 to 3. The search moves to position 1, where a* takes both letters a and stops before c. Both engines keep the match aa, from 1 to 3.
- The empty match at 3. At position 3, right where aa ended, a* can match nothing again. Go ignores an empty match that touches the match before it, as its documentation says. JavaScript keeps it as a match from 3 to 3.
- The empty match at the end. Position 4 is the end of the text, and a* matches nothing there too. No match ended at 4, so this empty match touches nothing and both engines keep it. Go has found three matches, JavaScript four.
- Replace each match with a dash. A replacement writes one dash for every match the search found. Go writes three and returns -b-c-; JavaScript writes four and returns -b--c-. The extra dash is the empty match at 3.
Go’s documentation states the rule in one sentence: “Empty matches abutting a preceding match are ignored.” The
empty match at position 3 begins exactly where aa ended, so Go never reports it. Python’s documentation says the
opposite, with an example of its own: there, an empty match may follow straight after a non-empty one. The runs below
print the matches, the replacement and the split in each language:
Go 1.27.1 · empty/empty.go
// a* can match nothing, so a search for it in "baac" finds empty matches besides aa. Go ignores an empty match that
// touches the match before it, which changes the list of matches and every replacement built on it.
package main
import (
"fmt"
"regexp"
)
func main() {
text := "baac"
star := regexp.MustCompile(`a*`)
fmt.Println("matches at", star.FindAllStringIndex(text, -1))
fmt.Printf("matches %q\n", star.FindAllString(text, -1))
fmt.Printf("replace %q\n", star.ReplaceAllString(text, "-"))
fmt.Printf("split %q\n", star.Split(text, -1))
fmt.Printf("on aaa %q\n", star.ReplaceAllString("aaa", "-"))
plus := regexp.MustCompile(`a+`)
fmt.Printf("a+ replace %q\n", plus.ReplaceAllString(text, "-"))
} Output
matches at [[0 0] [1 3] [4 4]] matches ["" "aa" ""] replace "-b-c-" split ["b" "c"] on aaa "-" a+ replace "b-c"
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run empty.go
Python 3.14.8 · empty/empty.py
"""The same searches in Python: an empty match right after aa is still a match here, and re.split cuts at every
match, the empty ones included."""
import re
text = "baac"
print("matches at", [m.span() for m in re.finditer(r"a*", text)])
print("matches ", re.findall(r"a*", text))
print("replace ", repr(re.sub(r"a*", "-", text)))
print("split ", re.split(r"a*", text))
print("on aaa ", repr(re.sub(r"a*", "-", "aaa")))
print("a+ replace", repr(re.sub(r"a+", "-", text))) Output
matches at [(0, 0), (1, 3), (3, 3), (4, 4)] matches ['', 'aa', '', ''] replace '-b--c-' split ['', 'b', '', 'c', ''] on aaa '--' a+ replace 'b-c'
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 empty.py
// The same searches in JavaScript: after aa ends at position 3, a global search still reports the empty match there,
// so the replacement writes two dashes between b and c.
const text = 'baac';
const spans = [...text.matchAll(/a*/g)].map((m) => [m.index, m.index + m[0].length]);
console.log('matches at', JSON.stringify(spans));
console.log('matches ', JSON.stringify(text.match(/a*/g)));
console.log('replace ', JSON.stringify(text.replace(/a*/g, '-')));
console.log('split ', JSON.stringify(text.split(/a*/)));
console.log('a+ replace', JSON.stringify(text.replace(/a+/g, '-'))); Output
matches at [[0,0],[1,3],[3,3],[4,4]] matches ["","aa","",""] replace "-b--c-" split ["b","c"] a+ replace "b-c"
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node empty.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
Side by side, the three runs give this table:
| Engine | Matches of a* in baac |
Replace with - |
Split |
|---|---|---|---|
| Go | 3: at 0, from 1 to 3, at 4 | -b-c- |
b, c |
| JavaScript | 4: at 0, from 1 to 3, at 3, at 4 | -b--c- |
b, c |
| Python | 4: at 0, from 1 to 3, at 3, at 4 | -b--c- |
'', b, '', c, '' |
Every Go method that walks the list of matches follows the rule, so a replacement, a split and FindAllString all
lose the same empty match. The split column differs for a second reason, since each language decides on its own which
empty pieces to keep; the quantifiers lesson shows the same differences on
axxb. The last line of each run is the way around them: a+ cannot match nothing, and all three engines return
b-c.
Predict Go on a text of only a's
Press Edit on the JavaScript run, change baac to aaa and run it again. JavaScript writes two dashes: one for
aaa and one for the empty match at the end, as the on aaa line of the Python tab does. The same line of the Go
tab holds a single dash, because that empty match starts where aaa ended.
A pattern that can match nothing is where the three engines part ways, so it is also where a port needs its test inputs:
Key idea
Go drops an empty match that starts where the previous match ended, so a pattern that can match nothing gives Go
fewer matches than JavaScript or Python; written with + instead of *, the same job gives one answer in all three.
Checkpoint
Check: Empty matches right after a match
Two switches that change the match, not the time limit
Every engine of this lesson so far picks the leftmost-first: A rule for choosing among matches: take the earliest start, then the match a backtracking search finds first, which follows the order of the alternatives. JavaScript, Python and PCRE2 use it. More in “How an engine finds a match, step by step” match: among the matches
that start earliest, the one a backtracking search would find first. Go offers a second rule. CompilePOSIX compiles
a pattern for leftmost-longest: A rule for choosing among matches: take the earliest start, then the longest match from it, whatever the order of the alternatives. POSIX specifies it. More in “How an engine finds a match, step by step” matching, the rule POSIX specifies, and the method
Longest switches an already compiled pattern to it. A tokenizer whose keywords are written shortest first shows the
difference, and the same run tries the U flag and the syntax CompilePOSIX accepts:
// Three switches that change which text matches, not how fast it is found: CompilePOSIX and Longest take the longest
// of the matches that start at the same place, and (?U) swaps the greedy and lazy forms of every quantifier.
package main
import (
"fmt"
"regexp"
)
func main() {
keywords := `in|int|interface`
first := regexp.MustCompile(keywords)
posix := regexp.MustCompilePOSIX(keywords)
longest := regexp.MustCompile(keywords)
longest.Longest()
for _, word := range []string{"interface", "int", "into"} {
fmt.Printf("%-9s Compile %-5q CompilePOSIX %-11q Longest %q\n",
word, first.FindString(word), posix.FindString(word), longest.FindString(word))
}
tags := "<b>bold</b> text"
for _, p := range []string{`<.+>`, `<.+?>`, `(?U)<.+>`, `(?U)<.+?>`} {
fmt.Printf("%-10s %q\n", p, regexp.MustCompile(p).FindString(tags))
}
// The U flag is not Java's Unicode switch: \w stays ASCII, and every match is one character long.
fmt.Printf("%-10s %q\n", `(?U)\w+`, regexp.MustCompile(`(?U)\w+`).FindAllString("go नमस्ते", -1))
// CompilePOSIX also narrows the syntax to POSIX extended regular expressions.
for _, p := range []string{`\d+`, `(?i)go`, `[0-9]+`, `[[:digit:]]+`} {
answer := "compiles"
if _, err := regexp.CompilePOSIX(p); err != nil {
answer = err.Error()
}
fmt.Printf("CompilePOSIX %-12s %s\n", p, answer)
}
} Output
interface Compile "in" CompilePOSIX "interface" Longest "interface" int Compile "in" CompilePOSIX "int" Longest "int" into Compile "in" CompilePOSIX "int" Longest "int" <.+> "<b>bold</b>" <.+?> "<b>" (?U)<.+> "<b>" (?U)<.+?> "<b>bold</b>" (?U)\w+ ["g" "o"] CompilePOSIX \d+ error parsing regexp: invalid escape sequence: `\d` CompilePOSIX (?i)go error parsing regexp: missing argument to repetition operator: `?` CompilePOSIX [0-9]+ compiles CompilePOSIX [[:digit:]]+ compiles
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run switches.go
On interface, in|int|interface gives in under Compile, because in is written first and matches, and
interface under CompilePOSIX and Longest, because it is the longest match from position 0.
How an engine finds a match compares the two rules on a|ab. CompilePOSIX
also narrows the syntax to POSIX extended expressions: the last lines show \d refused, and (?i) refused with a
message about a repetition operator, because in that syntax a question mark can only repeat what comes before it.
Write [0-9] or [[:digit:]] there instead. Both rules keep the package’s linear bound; only the match they report
changes.
The second switch is a flag. (?U) swaps the greedy and the lazy form of every quantifier: in the run, (?U)<.+>
stops at the first > and returns <b>, while (?U)<.+?> turns greedy and returns <b>bold</b>. regexp/syntax lists
the flag as U, ungreedy, and PCRE2 and PHP know the same ungreedy mode: An option of PCRE2, PHP (the U modifier) and Go, also set with (?U) in a pattern, that swaps the two forms: a quantifier without a question mark is lazy and one with it is greedy. JavaScript and Python have no such mode. More in “Greedy and lazy quantifiers”, which
greedy and lazy quantifiers compares across engines.
Reading Go's (?U) the way Java reads it
Wrong: adding (?U) to a Go pattern to make \w take letters of every script, as it does in Java, where the same
two characters switch on UNICODE_CHARACTER_CLASS (Java’s Pattern documentation). In Go they make every quantifier
lazy: on go नमस्ते the run’s (?U)\w+ line returns g and o and nothing of the Hindi word. Right: in Go, spell
the class out, for example [\pL\pM]+.
A reader who sees only the pattern sees neither switch: CompilePOSIX and Longest live in the code that compiles
it, and a (?U) at its start is easy to miss. Leave a comment where either one is set.
Key idea
CompilePOSIX and Longest make Go keep the longest of the matches that start earliest, and (?U) makes every
quantifier lazy; both change which text a pattern returns, never the bound on how long the search may take.
Checkpoint
Check: Leftmost-longest matching and (?U)
The same syntax in a browser, with JavaScript habits
A pattern that runs in a Go service often has to run in a page as well: a preview of a log filter, or a check on a form
before it is sent. The browser’s own RegExp is the JavaScript flavour, not RE2. re2js is a JavaScript library that
ports the RE2 engine; its README says it reaches full architectural parity with Go’s regexp package and guarantees
linear time in the browser and in Node.js. Set against Go, its README states this:
| What | Go regexp |
re2js, as its README states |
|---|---|---|
| Pattern syntax | RE2 | RE2; lookbehind without groups only with its LOOKBEHINDS flag, off by default |
| Time | linear in the input | linear in the input |
| Replacement strings | $1, ${1}, $name, ${name}, $$ |
$1, $<name>, $&, $$; ${name} only in its Java mode |
| Many patterns in one pass | no such type | RE2Set |
| An empty match right after a match | ignored | not stated |
The syntax and the time bound travel with the pattern, and the conventions around it do not. A template written for
Go’s ReplaceAllString meets JavaScript’s rules in re2js’s default mode, and the README says nothing about the
empty-match rule of the last section, so both belong in a differential test: the same patterns and the same texts
through both engines, with every output compared, as the porting lesson
builds one. Its RE2Set is a match set: A group of patterns compiled together so that one pass over a text reports which of them match, as RE2::Set in RE2 and RE2Set in re2js do. Go's regexp package has no such type., the idea behind RE2’s own RE2::Set: many
patterns compiled together and checked in one pass, which Go’s standard package does not offer.
Checkpoint
Check: RE2 syntax in a browser
Practise: filters that Go can compile
The exercise asks for five filters over an access log, written in JavaScript or Python, where every pattern must also be one Go would compile. Its sample tests check the lines each filter keeps and search each pattern for the constructs of the first section, and two of the filters need a few lines of code beside their pattern.
Exercise · Medium · JavaScript, Python
Log filters whose patterns also compile in Go
A log viewer runs the same five filters in two places: in the browser, and in a Go service that uses regexp. Write them in filters.mjs (JavaScript) or in filters.py (Python) so that every pattern you use would also compile in Go: no lookahead or lookbehind, no backreference, no possessive quantifier and no atomic group. Keep each pattern in PATTERNS; the tests read them there.
Each line of the log has the same fields, separated by single spaces: a time, a level (INFO, WARN, ERROR or FATAL), a method, a path, a status, a duration in milliseconds and a request id. A retried request also carries the id it retries:
09:14:04 ERROR POST /cart 503 1240ms req=b7c2
09:14:07 FATAL POST /pay 500 2050ms req=e5f6 retry=e5f6
Each function takes a list of lines and returns the lines it keeps, in their order:
failures: the lines whose level isERRORorFATAL. A path that contains the word, such as/docs/ERROR, does not make a line a failure.serverErrors(server_errorsin Python): the lines whose status is from 500 to 599. A duration such as512msis not a status.slow: the lines whose request took 1000 ms or more, with no upper limit.withoutHealthChecks(without_health_checks): every line except the requests for/healthand for the paths below it, such as/health/db. A path that only starts with the same letters, such as/healthcare/plans, stays.retries: the lines whoseretry=id is exactly their ownreq=id.
The starter works in JavaScript and in Python, but two of its patterns use constructs Go refuses, and some of its filters keep the wrong lines. Where a filter needs something an RE2 pattern cannot say, let the pattern find the part it can and do the rest in code.
JavaScript · Starter code · filters.mjs
// filters.mjs: five filters over the lines of an access log. Every pattern they use must be one that Go's regexp
// would compile too, so that the Go service can run the same rules. Keep each pattern in PATTERNS: the tests read it.
// Starter: these patterns work in JavaScript, but two of them use constructs Go refuses, and some filters keep the
// wrong lines.
export const PATTERNS = {
failure: /ERROR|FATAL/,
serverError: / 5\d\d/,
slow: / [1-9]\d{3}ms/,
notHealth: /^(?!.* \/health)/,
retry: / req=(\w+) retry=\1/,
};
/** The lines whose level is ERROR or FATAL. */
export function failures(lines) {
return lines.filter((line) => PATTERNS.failure.test(line));
}
/** The lines whose status is from 500 to 599. */
export function serverErrors(lines) {
return lines.filter((line) => PATTERNS.serverError.test(line));
}
/** The lines whose request took 1000 ms or more. */
export function slow(lines) {
return lines.filter((line) => PATTERNS.slow.test(line));
}
/** Every line except the requests for /health and the paths below it. */
export function withoutHealthChecks(lines) {
return lines.filter((line) => PATTERNS.notHealth.test(line));
}
/** The lines whose retry= id is exactly their own req= id. */
export function retries(lines) {
return lines.filter((line) => PATTERNS.retry.test(line));
} The sample tests · filters.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { PATTERNS, failures, serverErrors, slow, withoutHealthChecks, retries } from './filters.mjs';
const LOG = [
'09:14:03 INFO GET /orders/42 200 31ms req=a1f3',
'09:14:04 ERROR POST /cart 503 1240ms req=b7c2',
'09:14:05 INFO GET /health 200 2ms req=c9d0',
'09:14:06 WARN GET /orders/43 404 18ms req=d4e1',
'09:14:07 FATAL POST /pay 500 2050ms req=e5f6 retry=e5f6',
'09:14:08 INFO GET /docs/ERROR 200 512ms req=f0a1',
'09:14:09 INFO GET /health/db 200 3ms req=a2b3',
'09:14:10 INFO GET /healthcare/plans 200 999ms req=b3c4',
'09:14:11 WARN POST /pay 502 1000ms req=c4d5 retry=c4d6',
'09:14:12 INFO POST /pay 200 12000ms req=ab12 retry=ab123',
];
// The lines with these numbers, counting from 1, in order.
const lines = (...numbers) => numbers.map((n) => LOG[n - 1]);
// The constructs Go's regexp refuses to compile, found in the source text of a pattern.
function notInGo(source) {
const found = [];
const kept = source.replace(/\\\\/g, ''); // an escaped backslash starts nothing
const plain = kept.replace(/\\./g, '_'); // an escaped character is a literal
if (/\\[1-9]|\\k</.test(kept) || /\(\?P=/.test(plain)) found.push('a backreference');
if (/\(\?<?[=!]/.test(plain)) found.push('a lookaround');
if (/\(\?>/.test(plain)) found.push('an atomic group');
if (/[*+?}]\+/.test(plain)) found.push('a possessive quantifier');
return found;
}
test('failures: the level is ERROR or FATAL, not a word in the path', () => {
assert.deepEqual(failures(LOG), lines(2, 5));
});
test('serverErrors: a status from 500 to 599, not a duration such as 512ms', () => {
assert.deepEqual(serverErrors(LOG), lines(2, 5, 9));
});
test('slow: 1000 ms or more, with no upper limit', () => {
assert.deepEqual(slow(LOG), lines(2, 5, 9, 10));
});
test('withoutHealthChecks: drops /health and /health/db, keeps /healthcare/plans', () => {
assert.deepEqual(withoutHealthChecks(LOG), lines(1, 2, 4, 5, 6, 8, 9, 10));
});
test('retries: the retry= id is exactly the req= id', () => {
assert.deepEqual(retries(LOG), lines(5));
});
test('every pattern in PATTERNS is one Go would compile', () => {
const names = Object.keys(PATTERNS);
assert.ok(names.length >= 5, 'PATTERNS should hold the pattern of every filter');
for (const name of names) {
const pattern = PATTERNS[name];
assert.ok(pattern instanceof RegExp, `PATTERNS.${name} should be a regular expression`);
assert.deepEqual(notInGo(pattern.source), [], `PATTERNS.${name} is /${pattern.source}/`);
}
}); Python · Starter code · filters.py
"""filters.py: five filters over the lines of an access log. Every pattern they use must be one that Go's regexp
would compile too, so that the Go service can run the same rules. Keep each pattern in PATTERNS: the tests read it."""
import re
# Starter: these patterns work in Python, but two of them use constructs Go refuses, and some filters keep the wrong
# lines.
PATTERNS = {
"failure": re.compile(r"ERROR|FATAL"),
"server_error": re.compile(r" 5\d\d"),
"slow": re.compile(r" [1-9]\d{3}ms"),
"not_health": re.compile(r"^(?!.* /health)"),
"retry": re.compile(r" req=(\w+) retry=\1"),
}
def failures(lines):
"""The lines whose level is ERROR or FATAL."""
return [line for line in lines if PATTERNS["failure"].search(line)]
def server_errors(lines):
"""The lines whose status is from 500 to 599."""
return [line for line in lines if PATTERNS["server_error"].search(line)]
def slow(lines):
"""The lines whose request took 1000 ms or more."""
return [line for line in lines if PATTERNS["slow"].search(line)]
def without_health_checks(lines):
"""Every line except the requests for /health and the paths below it."""
return [line for line in lines if PATTERNS["not_health"].search(line)]
def retries(lines):
"""The lines whose retry= id is exactly their own req= id."""
return [line for line in lines if PATTERNS["retry"].search(line)] The sample tests · test_filters.py
import re
from filters import PATTERNS, failures, retries, server_errors, slow, without_health_checks
LOG = [
"09:14:03 INFO GET /orders/42 200 31ms req=a1f3",
"09:14:04 ERROR POST /cart 503 1240ms req=b7c2",
"09:14:05 INFO GET /health 200 2ms req=c9d0",
"09:14:06 WARN GET /orders/43 404 18ms req=d4e1",
"09:14:07 FATAL POST /pay 500 2050ms req=e5f6 retry=e5f6",
"09:14:08 INFO GET /docs/ERROR 200 512ms req=f0a1",
"09:14:09 INFO GET /health/db 200 3ms req=a2b3",
"09:14:10 INFO GET /healthcare/plans 200 999ms req=b3c4",
"09:14:11 WARN POST /pay 502 1000ms req=c4d5 retry=c4d6",
"09:14:12 INFO POST /pay 200 12000ms req=ab12 retry=ab123",
]
def lines(*numbers):
"""The lines with these numbers, counting from 1, in order."""
return [LOG[n - 1] for n in numbers]
def not_in_go(source):
"""The constructs Go's regexp refuses to compile, found in the source text of a pattern."""
found = []
kept = source.replace("\\\\", "") # an escaped backslash starts nothing
plain = re.sub(r"\\.", "_", kept) # an escaped character is a literal
if re.search(r"\\[1-9]|\\k<", kept) or "(?P=" in plain:
found.append("a backreference")
if re.search(r"\(\?<?[=!]", plain):
found.append("a lookaround")
if "(?>" in plain:
found.append("an atomic group")
if re.search(r"[*+?}]\+", plain):
found.append("a possessive quantifier")
return found
def test_failures():
"""failures: the level is ERROR or FATAL, not a word in the path"""
assert failures(LOG) == lines(2, 5)
def test_server_errors():
"""server_errors: a status from 500 to 599, not a duration such as 512ms"""
assert server_errors(LOG) == lines(2, 5, 9)
def test_slow():
"""slow: 1000 ms or more, with no upper limit"""
assert slow(LOG) == lines(2, 5, 9, 10)
def test_without_health_checks():
"""without_health_checks: drops /health and /health/db, keeps /healthcare/plans"""
assert without_health_checks(LOG) == lines(1, 2, 4, 5, 6, 8, 9, 10)
def test_retries():
"""retries: the retry= id is exactly the req= id"""
assert retries(LOG) == lines(5)
def test_every_pattern_compiles_in_go():
"""every pattern in PATTERNS is one Go would compile"""
assert len(PATTERNS) >= 5, "PATTERNS should hold the pattern of every filter"
for name, pattern in PATTERNS.items():
assert isinstance(pattern, re.Pattern), f"PATTERNS[{name!r}] should be a compiled pattern"
assert not_in_go(pattern.pattern) == [], f"PATTERNS[{name!r}] is {pattern.pattern!r}" A hint
Two of the filters cannot be a single RE2 pattern. "Every line except" is a negative condition: write a pattern for the lines you want to drop and keep the lines where it does not match. "The same id twice" needs a backreference: capture both ids with two groups and compare the two strings in code. For the other three, let the fields around the value do the work: a status has a space on both sides, and a duration ends in ms. Write [0-9] for a digit, since \d is not the same set in every engine.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then comes the self-check. It asks first about the idea you have shown least so far, and every choice it offers says why it is right or what it misses:
Practice that adapts to you
Practise this lesson
14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up: which constructs does Go’s regexp refuse, and what does it get in return?
It refuses whatever needs more than a set of positions to match: backreferences such as \1 and \k<name>, lookahead
and lookbehind, possessive quantifiers and atomic groups, a few escapes such as \Z, \G and \X, the x flag, and
counted repetitions above 1,000. In return the package can promise that a match takes time linear in the length of
the text, whatever the pattern, because it follows every path through the compiled pattern at once instead of trying
one and backing up. regexp.Compile returns the refusal as an error, such as invalid or unsupported Perl syntax, so
a bad pattern is caught when the program builds it. A lookbehind gives a misleading message, invalid named capture,
because (?< starts a named group in Go. Whatever a refused construct did moves into code: a second pattern, or a
comparison of two groups.
Two design questions take these rules into real work: moving a text-rewriting job from Python to Go, and letting users type their own filter patterns. Each row below opens on its first step; decide yours before you read it.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A nightly Python job rewrites order references with
re.sub(r"(?P<site>[A-Z]{3})-(\d+)", r"\g<site>_\2", text)and reads the fields of each line withre.findall(r"[^,]*", line). It moves to a Go service. Which results change without any error, how do you write the Go version, and how do you show that it matches? -
Mid to staff
A log viewer lets users save their own filter patterns. The page previews a filter with the browser's RegExp, and a Go service runs the saved filters on every incoming line. Users report filters that preview fine but fail to save, and the team wants no saved filter ever to stall the service. How do you design it?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does Go's regexp promise about match time, and what does it refuse in exchange?
Every match is bounded by the compiled pattern's size times the text's length, because it follows all paths at once. It refuses backreferences, lookaround, possessive and atomic forms and counts above 1,000 when the pattern compiles.
-
What does Go's error invalid named capture: (?<=a) really point at?
A lookbehind. Go's parser reads (?< as the start of a named group, so it complains about the name. Rewrite that part with a capture group or a check in code.
-
What does Go's FindAllStringSubmatchIndex return for each match?
Byte offsets in pairs: group k starts at place 2k and ends at place 2k + 1, with -1 for a group that took no part.
-
Why does the Go template $1xxxx delete text without any error?
$name takes the longest name it can, so Go looks for a group named 1xxxx and writes nothing. Write ${1}xxxx, use $$ for a dollar sign, or send the text through ReplaceAllLiteralString or ReplaceAllStringFunc.
-
Why does replacing a* with a hyphen give -b-c- on baac in Go but -b--c- in JavaScript and Python?
Go ignores an empty match that starts where the previous match ended. Written with + instead of *, the same job gives one answer in all three.
-
What do CompilePOSIX, Longest and (?U) change in Go?
CompilePOSIX and Longest switch to leftmost-longest matching, and (?U) makes quantifiers lazy by default. They change which text matches, never the time bound. In Java the same (?U) means Unicode character classes.
-
What does re2js carry into the browser, and what does a pattern shared with Go still need?
RE2's syntax and its linear bound, with JavaScript-style replacement strings. A pattern shared with Go still needs a differential test of its outputs.
Key takeaways
- Go’s
regexpbounds every match by the compiled pattern’s size times the text’s length, because it follows all paths at once, and pays for it by refusing backreferences, lookaround, possessive and atomic forms and counts above 1,000 when the pattern compiles. - The finding methods are named Find or FindAll, then String, Submatch and Index, and
FindAllStringSubmatchIndexreturns byte offsets in pairs, group k at places 2k and 2k + 1, with -1 for a group that took no part. - In a Go template,
$nametakes the longest name it can, so$1xxxxdeletes text without an error;${1},$$,ReplaceAllLiteralStringandReplaceAllStringFuncsay what you mean. - Go ignores an empty match that starts where the previous match ended, so
a*gives-b-c-onbaacwhere JavaScript and Python give-b--c-, and+makes the three agree. CompilePOSIXandLongestswitch Go to leftmost-longest matching, and(?U)makes quantifiers lazy by default, which is not what the same flag means in Java.- re2js carries RE2’s syntax and linear bound into the browser with JavaScript-style replacement strings, so a pattern shared with Go needs a differential test of its outputs.
Terms in this lesson
- linear-time matching
- A guarantee that, for a fixed pattern, the time a match takes grows at most in proportion to the length of the text, whatever the text holds. Go's regexp gives it by following every path of the pattern at once.
- match set
- A group of patterns compiled together so that one pass over a text reports which of them match, as RE2::Set in RE2 and RE2Set in re2js do. Go's regexp package has no such type.
- Pike VM
- A way to run a compiled pattern that keeps at most one thread per instruction, each with its own group offsets, and moves them all forward one character at a time. Rob Pike first used it in the sam editor.
- RE2
- A regular-expression library and syntax from Google whose primary guarantees include match time linear in the input, which is why it leaves out backreferences and lookaround. Go's regexp package accepts its syntax.
References
- regexp: package overview, the linear-time guarantee and the names of the Find methods (The Go Authors)
- regexp: Compile (leftmost-first) and CompilePOSIX (leftmost-longest) (The Go Authors)
- regexp: Regexp.Expand, the $name and ${name} template syntax (The Go Authors)
- regexp: Regexp.FindString, empty match or no match (The Go Authors)
- regexp/syntax: the syntax Go accepts, its flags and the repetition limit (The Go Authors)
- Go 1.27.1 source: regexp/backtrack.go (the bounded backtracker) (The Go Authors)
- Go 1.27.1 source: regexp/exec.go (threads with their capture arrays) (The Go Authors)
- RE2, a regular expression library (README): safety and the linear-time guarantee (Google)
- RE2 syntax (Google)
- RE2 source: re2/set.h (RE2::Set) (Google)
- Regular Expression Matching Can Be Simple And Fast (Russ Cox)
- Regular Expression Matching: the Virtual Machine Approach (Russ Cox)
- re2js: a JavaScript port of RE2 (README) (le0pard (re2js))
- re: re.sub(), its templates and empty matches (Python Software Foundation)
- ECMAScript 2026: GetSubstitution, the rules of a replacement template (Ecma International (TC39))
- Pattern (Java SE 25): UNICODE_CHARACTER_CLASS and the embedded flag (?U) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress