Skip to content
shellhacks.dev
Regular Expressions

How to Use Regex in Go regexp Package the Right Way

Use Go's regexp package: RE2 syntax, compile once with MustCompile, find and replace with submatches, named groups, flags, QuoteMeta and what RE2 leaves out.

6 min read
Summarize in AI
Illustration for "How to Use Regex in Go regexp Package the Right Way": a terminal session, gears, a regular expression and its match.

Go chose a regular expression engine that cannot be made to hang: RE2 guarantees that matching takes time proportional to the length of the input, whatever the pattern. The price is the missing features, and the benefit is that you can accept patterns from users safely. This post covers the daily operations, for programs like the grep clone. Targets Go 1.24.

Compile once, match

main.goGo
package main

import (
	"fmt"
	"regexp"
)

var logLine = regexp.MustCompile(`^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) ([A-Z]+) (.*)$`)

func main() {
	line := "2026-11-03 09:01:10 ERROR upstream timeout"
	fmt.Println(logLine.MatchString(line))

	m := logLine.FindStringSubmatch(line)
	if m != nil {
		fmt.Println(m[3], "|", m[4])
	}
}
Text
true
ERROR | upstream timeout

Backtick strings are raw, so there is no double escaping of backslashes. Compile patterns once, typically in a package-level var with MustCompile, which panics at start-up if the constant is invalid. Use regexp.Compile, which returns an error, for patterns that come from input. Compiling inside a loop wastes time. Index 0 of the submatch result is the whole match and the groups follow.

Named groups

Go
re := regexp.MustCompile(`(?P<key>\w+)=(?P<val>\S+)`)
m := re.FindStringSubmatch("id=4821 name=ada")
for i, name := range re.SubexpNames() {
	if name != "" {
		fmt.Println(name, "=", m[i])
	}
}
fmt.Println(re.SubexpIndex("val"))
Text
key = id
val = 4821
2

The syntax is (?P<name>...); Go 1.22 also accepts (?<name>...). SubexpNames maps positions to names, and SubexpIndex (Go 1.15 and later) finds the index of a name. Note that the example finds only the first match; for all, use the next function.

All matches and replacement

Go
all := re.FindAllStringSubmatch("id=4821 name=ada", -1)
fmt.Println(len(all), all[1][1])

out := re.ReplaceAllString("id=4821 name=ada", "${val}:${key}")
fmt.Println(out)

fn := re.ReplaceAllStringFunc("a=1 b=2", func(s string) string { return "<" + s + ">" })
fmt.Println(fn)
Text
2 name
4821:id ada:name
<a=1> <b=2>

The second argument -1 means "no limit". In replacement strings, $1 is read greedily as the longest name, so $1x means a group called 1x, which does not exist and expands to empty; write ${1}x. Use ReplaceAllLiteralString to insert text with no $ processing. The function variant receives the matched text only, not the groups.

Flags and literal text

Go
ci := regexp.MustCompile(`(?i)error|warn`)
user := "a.b*c"
lit := regexp.MustCompile(regexp.QuoteMeta(user))
fmt.Println(ci.MatchString("WARNING"), lit.MatchString("xa.b*cx"))

Flags go inside the pattern: (?i) ignores case, (?m) makes anchors per line and (?s) lets the dot match a newline. QuoteMeta escapes every special character in a string, which is how you turn user text into a literal pattern. For a plain substring check, strings.Contains is faster and simpler.

What RE2 does not have

No backreferences (\1 inside the pattern), no lookahead and no lookbehind, and no possessive quantifiers or atomic groups, since the engine does not backtrack. A pattern that needs them has to be split: match a wider candidate, then filter in Go code. The restriction also means the engine cannot be hung by a hostile pattern like (a+)+$, unlike Python or PCRE; see the Python re module for the contrast. Syntax details are in regex differences across tools.

Where this does not apply

Parsing structured text (JSON, URLs, HTML) belongs to the dedicated packages (encoding/json, net/url). For very hot loops, hand-written scanning or bytes functions can beat regexp, which is not the fastest engine; measure before replacing. Use Match and Find on byte slices to avoid conversions when reading files.

The engine and its guarantees

Go's regexp package implements the RE2 syntax, and the guarantee that comes with it is that matching runs in time linear in the size of the input, whatever the pattern. That removes the exponential blow-up that backtracking engines suffer, and makes the package safe for patterns supplied by users, which is a real reason to choose it for a service. The price is a smaller feature set. There are no backreferences and no lookahead or lookbehind assertions, since those cannot be done in linear time. A pattern copied from a Perl or Python source that uses them fails at compile time with an error that names the unsupported construct, and the rewrite usually involves capturing the context you wanted to assert and checking it in code. Lazy quantifiers are supported, with a question mark after the quantifier, and so are non-capturing groups and inline flags such as (?i) for case-insensitive matching, (?s) for a dot that matches newlines and (?m) for multi-line anchors.

Compile a pattern once, at package level with regexp.MustCompile for a constant pattern, and reuse it. Compiling inside a loop repeats work and slows the program by orders of magnitude on large inputs.

Substitution syntax and its snags

In ReplaceAllString, the replacement may refer to groups with $1 or ${1} and named groups with ${name}. The unbraced form reads as many characters as could be part of a name, so $1x means the group called 1x, which does not exist and expands to nothing. Write ${1}x when text follows the reference. A literal dollar sign in the replacement is written $$. If the replacement should be taken literally, with no group expansion at all, use ReplaceAllLiteralString, which is the right choice for text from outside your control. For computed replacements, ReplaceAllStringFunc receives the matched text and returns the new text, though it does not give access to groups, so for that you call FindStringSubmatchIndex yourself. Named groups can be written (?P<name>...) and, since Go 1.22, also (?<name>...), and SubexpNames lists them in order.

To embed user text in a pattern as a literal, wrap it with regexp.QuoteMeta, which escapes every special character, and then combine it with the rest of the pattern.

Finding every match and working with bytes

The FindAll family returns every match, with a limit argument where a negative number means no limit. There are variants for strings and byte slices, for the matched text and for the indexes, and for submatches. Using the index variants and slicing the original string avoids copying. The default is leftmost-first matching, as in Perl, and the Longest method switches to POSIX leftmost-longest, which changes results for patterns with alternation, so choose it only when you need the POSIX behavior. The package works on UTF-8 text, and a dot matches one code point and not one byte. For binary data, use the byte-slice functions and a pattern that does not assume valid UTF-8. A common performance advice is to prefer simple string functions, such as strings.Contains and strings.HasPrefix, when a pattern is just literal text, since they are much faster than the regex machinery.

Testing and tuning patterns in Go

Test patterns with table-driven tests that list inputs, whether each should match and the expected groups, and run them with the race detector and with fuzzing where the input comes from outside, since Go's fuzzing support finds inputs that panic the surrounding code, even though the engine itself cannot backtrack catastrophically. Benchmark a hot path with the standard benchmark tooling and compare against plain string functions. A match against a long literal prefix is much faster with strings.HasPrefix, and a regular expression that begins with a literal prefix is optimized by the engine as well, so putting the fixed part first helps. For very large inputs, work on streams with bufio and apply the pattern to each line, since loading a file into one string and matching across it can use far more memory than the task needs.

Keep error handling honest. Use regexp.Compile and return the error for patterns that come from configuration or users, and reserve MustCompile, which panics, for constants known at build time.

See the regexp package and the RE2 syntax reference.

Also inGo
Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.