Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Regex in Golang: RE2, MustCompile, and the Backreference You Cannot Have

Sean

Platform Writer

Aug 27, 2026
8 min read

Go’s regexp package is built on RE2, which trades two features you may miss for a guarantee you probably want: no backreferences, no lookarounds, and no pattern can ever blow up into exponential runtime.

Regex in Golang: RE2, MustCompile, and the Backreference You Cannot Have

Most regex engines, including Perl’s and Python’s, use backtracking. That buys backreferences and lookarounds, and it costs you the possibility that a crafted input turns a harmless-looking pattern into a hang. Go picked the other side of that trade, and the consequences show up the first time you paste in a pattern from somewhere else and it refuses to compile.

Table of contents

Compile once, at package level

The single most common performance mistake with Go regex is compiling inside a hot function. Compilation is expensive; matching is cheap. Do it once.

package main

import (
    "fmt"
    "regexp"
)

// Compiled once, at init. Panics at startup if the pattern is bad.
var slugRe = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)

func IsSlug(s string) bool {
    return slugRe.MatchString(s)
}

MustCompile panics on an invalid pattern, which sounds harsh until you notice where it happens: at package initialisation, so a typo in a regex literal fails the process on startup rather than on the first request that touches it. That is the behaviour you want for patterns you wrote yourself.

Use regexp.Compile and handle the error only when the pattern comes from outside the binary - a config file, a database column, or user input. In that case, also consider what a user-supplied pattern can do to your CPU budget, though RE2 bounds that far more tightly than a backtracking engine would.

Backticks matter here. A raw string literal means you write the escape sequences once instead of doubling every backslash, and regex patterns are dense enough already.

The methods, and how they are named

The regexp API looks large but it is almost entirely combinatorial. The method names are built from a small set of parts, and once you see the pattern you can guess any of them.

  • Find returns the leftmost match.
  • All means every match, and the method takes an n limit, with -1 meaning unlimited.
  • String means work on and return strings rather than byte slices.
  • Submatch means include capture groups.
  • Index means return positions rather than the matched text.
re := regexp.MustCompile(`(\w+)@(\w+)\.com`)
s := "mail [email protected] and [email protected]"

re.FindString(s)         // [email protected]
re.FindAllString(s, -1)  // [[email protected] [email protected]]
re.FindStringSubmatch(s) // [[email protected] alice example]
re.FindStringIndex(s)    // [5 22]
re.MatchString(s)        // true

FindStringSubmatch returns a slice where index 0 is the whole match and the rest are capture groups in order. It returns nil when there is no match, so check length before indexing - a missing match indexed at position one is a panic, and it is the most common regex crash in Go code.

Named groups are worth the extra characters

Positional capture groups break the moment someone adds a group in the middle of the pattern. Named groups do not.

var logRe = regexp.MustCompile(
    `^(?P<level>\w+)\s+(?P<ts>[\d:T-]+)\s+(?P<msg>.*)$`)

func parse(line string) map[string]string {
    m := logRe.FindStringSubmatch(line)
    if m == nil {
        return nil
    }
    out := make(map[string]string)
    for i, name := range logRe.SubexpNames() {
        if i > 0 && name != "" {
            out[name] = m[i]
        }
    }
    return out
}

SubexpNames() returns a slice parallel to the submatch slice, with an empty string at index 0 and for any unnamed group. The loop above is boilerplate you will write once and copy forever. SubexpIndex is the shorter route when you only need one group.

Go uses the Python-style named group syntax, not the shorter form some other engines accept. Patterns copied from JavaScript will need this adjusted.

Replacement, and the dollar-sign trap

ReplaceAllString expands group references in the replacement text. The brace form exists for a real reason.

re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)

re.ReplaceAllString("2026-08-27", "$3/$2/$1")
// 27/08/2026

// The trap: $1x is parsed as a group named 1x, which does not exist
re.ReplaceAllString("2026-08-27", "$1x")   // empty string
re.ReplaceAllString("2026-08-27", "${1}x") // 2026x

Go reads the longest possible name after the dollar sign, so any alphanumeric character touching the group number becomes part of the name. Always use the brace form when the replacement continues with a letter or digit. If you want a literal dollar sign in the output, double it.

When the replacement needs logic rather than a template, ReplaceAllStringFunc hands you each match and takes whatever you return.

re := regexp.MustCompile(`\b[A-Z]{2,}\b`)
out := re.ReplaceAllStringFunc(text, strings.ToLower)

What RE2 will not do

RE2 guarantees that matching runs in time linear in the length of the input. It gets that guarantee by refusing any construct that requires backtracking, which rules out two families of pattern.

  • Backreferences - matching a repeated capture, such as finding a doubled word. Not supported.
  • Lookahead and lookbehind - positive or negative assertions in either direction. Not supported.

regexp.Compile returns an error for these rather than silently misbehaving, so you find out immediately. The workaround is almost always to match a looser pattern and apply the condition in Go code.

// Wanted: passwords with a digit, expressed as lookahead. Not possible.
// Instead, check the conditions separately.
var hasDigit = regexp.MustCompile(`[0-9]`)
var hasUpper = regexp.MustCompile(`[A-Z]`)

func strong(pw string) bool {
    return len(pw) >= 12 && hasDigit.MatchString(pw) && hasUpper.MatchString(pw)
}

That version is longer and considerably easier to read six months later, which is the quiet argument for the whole design. The guarantee you get in exchange is that no user-supplied input can turn a validation regex into a denial of service - the catastrophic backtracking class of bug simply does not exist here.

Before reaching for a regex at all, check the strings package. Contains, HasPrefix, Split, and Cut are faster, clearer, and cover a surprising share of what people write patterns for.

How this fits the rest of the stack

Regex-heavy request paths are where CPU limits stop being abstract - a validation pass that is fine at ten requests a second is a different story at a thousand. The RunxBuild hosting calculator puts the vCPU and RAM of each plan next to the database and bandwidth lines, so you can size the service against real throughput. Autoscaling between a floor and a ceiling plan covers the spikes without you watching a graph.

Useful related references:

FAQ

Why does my regex fail to compile in Go?

Almost always because it uses a backreference or a lookahead. Go’s regexp package implements RE2, which excludes both to guarantee linear-time matching. Those patterns return an error from Compile and panic in MustCompile. Split the condition into a simpler pattern plus a check in Go code.

What is the difference between Compile and MustCompile?

Compile returns a regexp and an error; MustCompile panics instead. Use MustCompile for patterns written as literals in your source, assigned to package-level variables - a bad pattern then fails at startup. Use Compile when the pattern comes from configuration or user input at runtime.

Is Go regex slow?

Compilation is relatively expensive and matching is fast, so the usual cause of slow regex code in Go is calling MustCompile inside a function that runs per request. Hoist it to a package-level variable. RE2 also has no catastrophic backtracking, so worst-case performance is far more predictable than backtracking engines.

How do I use named capture groups in Go?

Use the Python-style named group syntax in the pattern, not the shorter form used by some other engines. Retrieve values by pairing FindStringSubmatch with SubexpNames, or use SubexpIndex to get a single group’s position directly.

Should I use regex or the strings package?

Prefer strings when the operation is a fixed substring, prefix, suffix, or split on a constant separator - Contains, HasPrefix, Split, and Cut are faster and clearer. Reach for regexp when the pattern genuinely varies, such as validating a format or extracting fields from semi-structured text.

#regex golang#go regexp#MustCompile#RE2#go pattern matching