Backreferences in Go: Why `\1` Doesn't Compile, and What to Do Instead
Go's regexp package can't compile \1-style backreferences — here's the RE2 reason and the capture-and-verify fix.
The error that brought you here:
error parsing regexp: invalid escape sequence: `\1`Go's regexp package doesn't support backreferences — \1 through \9 simply aren't part of the language it accepts. Like lookarounds, this is a deliberate consequence of Go using RE2, not a gap waiting to be filled.
Why RE2 refuses
RE2 guarantees linear-time matching by compiling patterns to finite automata instead of backtracking. A backreference says "match whatever group 1 happened to capture, again" — the pattern's meaning now depends on runtime match state, which no finite automaton can express. Supporting \1 requires backtracking, and backtracking opens the door to catastrophic blowup (matching (a+)+b against "aaaaaaaaaaaaaaaaaaaaaaaaaaaa" can take exponential time in PCRE-style engines). RE2 keeps the guarantee and drops the feature. Backreferences are formally not regular expressions — RE2 is one of the few engines strict about that.
The universal workaround: capture, then verify in code
Every backreference pattern splits into two steps: match a loosened pattern with plain captures, then check the "same text again" constraint in Go.
Example 1 — duplicate words
The classic (duplicate word pattern), in PCRE:
\b(\w+)\s+\1\bGo version — capture both words, compare in code:
re := regexp.MustCompile(`\b(\w+)\s+(\w+)\b`)
for _, m := range re.FindAllStringSubmatch(text, -1) {
if m[1] == m[2] {
fmt.Println("duplicate:", m[1])
}
}One real difference from the PCRE original: overlapping candidates. FindAllStringSubmatch won't re-test a word it already consumed, so in "the the the" the third word isn't re-paired. When that matters, scan word-by-word instead:
words := regexp.MustCompile(`\w+`).FindAllString(text, -1)
for i := 1; i < len(words); i++ {
if strings.EqualFold(words[i], words[i-1]) {
fmt.Println("duplicate:", words[i])
}
}Shorter, faster, and case-insensitivity costs one function call instead of a regex flag.
Example 2 — matching paired delimiters
The PEM certificate block uses a backreference to require the END label to match the BEGIN label:
-----BEGIN ([A-Z ]+)-----([\s\S]+?)-----END \1-----Go version — capture both labels, verify equality:
re := regexp.MustCompile(
`-----BEGIN ([A-Z ]+)-----([\s\S]+?)-----END ([A-Z ]+)-----`)
for _, m := range re.FindAllStringSubmatch(pemData, -1) {
if m[1] == m[3] {
label, body := m[1], m[2]
_ = label
_ = body
}
}(For real PEM parsing, prefer encoding/pem from the standard library — which is the recurring theme: Go's answer to hard regex is usually a parser.)
Example 3 — HTML tags: stop before you start
The HTML tag matcher (<([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>([\s\S]*?)<\/\1>) uses \1 to pair closing tags with opening tags. You can port it with the capture-and-verify trick, but nested same-name tags break it in every engine — that's a regex limitation, not a Go limitation. In Go use golang.org/x/net/html; the code ends up shorter than the regex explanation.
Decision table
| Your backreference was... | Do this in Go |
|---|---|
Adjacent repetition ((\w+)\s+\1) | Capture twice, compare in code — or iterate tokens |
| Paired labels/delimiters | Capture both ends, verify equality |
Quote matching ((['"]).*?\1) | Two alternatives: "[^"]*" | '[^']*' |
| HTML/XML tag pairing | A real parser (x/net/html, encoding/xml) |
| Untranslatable user-supplied PCRE | github.com/dlclark/regexp2 — with MatchTimeout set, never on untrusted patterns |
The quote case deserves the one-liner: (['"]).*?\1 becomes "[^"]*"|'[^']*' — no backreference needed. One behavioral difference to know about: [^"]* happily matches across newlines, while .*? stops at line breaks unless dot-all mode is on. For single-line strings they're equivalent; for multi-line input, decide which behavior you actually want before swapping.
Test it live
All linked patterns run side by side in JS, Python, and Go's actual RE2 engine in RegexPro's tester — see exactly which engine accepts what.
Related guides: Lookarounds in Go · Named backreferences in Go