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:

textOutput
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:

textText
\b(\w+)\s+\1\b

Go version — capture both words, compare in code:

goGo
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:

goGo
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:

textText
-----BEGIN ([A-Z ]+)-----([\s\S]+?)-----END \1-----

Go version — capture both labels, verify equality:

goGo
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/delimitersCapture both ends, verify equality
Quote matching ((['"]).*?\1)Two alternatives: "[^"]*" | '[^']*'
HTML/XML tag pairingA real parser (x/net/html, encoding/xml)
Untranslatable user-supplied PCREgithub.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

All guidesOpen the RegexPro tester →