Regular expressions¶
Go's regexp package implements RE2, not the Perl-compatible dialect
Python uses. Most patterns you already know work unchanged; two features
are missing on purpose, and that is the part worth knowing up front.
var semver = regexp.MustCompile(`^v(\d+)\.(\d+)\.(\d+)$`)
fmt.Println(semver.MatchString("v1.22.3")) // output: true
fmt.Println(semver.MatchString("1.22.3")) // output: false
Compile once, at package level¶
Compiling a pattern is expensive; matching with it is cheap. A compiled
*regexp.Regexp is safe for concurrent use, so the idiom is a
package-level variable built with MustCompile, which panics on a bad
pattern:
Panicking is the right behaviour here: the pattern is a constant in your
source, so a broken one is a bug that should stop the program at startup
rather than at the first request. Use regexp.Compile and handle the
error only when the pattern comes from outside the program:
_, err := regexp.Compile(`[a-`)
fmt.Println(err)
// output: error parsing regexp: missing closing ]: `[a-`
Note the backquoted string: a
raw string literal, where a
backslash is just a backslash. In a regular "..." literal you would
have to write \\d for every \d, so raw strings are all but mandatory
for patterns.
Matching and extracting¶
MatchString answers yes or no. FindStringSubmatch returns the whole
match at index 0 and each capture group after it — or nil when there
is no match:
m := semver.FindStringSubmatch("v1.22.3")
fmt.Printf("%q\n", m) // output: ["v1.22.3" "1" "22" "3"]
fmt.Println(semver.FindStringSubmatch("nope") == nil) // output: true
There is no ok second result, so the nil check is the match check.
Indexing m[1] without it panics on no match.
Named groups use (?P<name>...), and SubexpIndex turns the name back
into a position:
named := regexp.MustCompile(`(?P<key>\w+)=(?P<val>\w+)`)
m := named.FindStringSubmatch("mode=fast")
fmt.Println(named.SubexpIndex("val"), m[named.SubexpIndex("val")])
// output: 2 fast
That is clumsier than Python's m.group("val"), and it is the main
ergonomic cost of the package.
Finding every match¶
The All variants take a count, where -1 means "no limit":
word := regexp.MustCompile(`\w+`)
fmt.Printf("%q\n", word.FindAllString("a bb ccc", -1)) // output: ["a" "bb" "ccc"]
fmt.Printf("%q\n", word.FindAllString("a bb ccc", 2)) // output: ["a" "bb"]
fmt.Println(word.FindString(" hi there")) // output: hi
fmt.Println(word.FindStringIndex(" hi there")) // output: [2 4]
FindString returns "" for no match, which is ambiguous when the
pattern can match an empty string — FindStringIndex returning nil is
the unambiguous form.
Replacing¶
In the replacement string, $1 and ${name} refer to capture groups:
date := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
fmt.Println(date.ReplaceAllString("on 2026-09-20 ok", "$3/$2/$1"))
// output: on 20/09/2026 ok
Use the braces whenever a digit could run into following text. $3x
would be read as the group named 3x, which does not exist and expands
to nothing:
When the replacement needs real logic, ReplaceAllStringFunc hands you
each match:
fmt.Println(word.ReplaceAllStringFunc("go rocks", func(s string) string {
return "<" + s + ">"
}))
// output: <go> <rocks>
Splitting takes a count for the same reason the All functions do:
sp := regexp.MustCompile(`\s*,\s*`)
fmt.Printf("%q\n", sp.Split("a ,b, c", -1)) // output: ["a" "b" "c"]
Flags go inside the pattern¶
There is no flags argument. (?i), (?m) and (?s) go at the start of
the pattern instead:
fmt.Println(regexp.MustCompile(`(?i)^go$`).MatchString("GO")) // output: true
fmt.Printf("%q\n", regexp.MustCompile(`(?m)^\w+`).FindAllString("one\ntwo", -1))
// output: ["one" "two"]
fmt.Println(regexp.MustCompile(`(?s)a.b`).MatchString("a\nb")) // output: true
(?i) is case-insensitive, (?m) makes ^/$ match at line
boundaries, (?s) lets . match a newline.
What RE2 will not do¶
Backreferences and lookaround are absent, and they are absent by design. RE2 guarantees matching in time linear in the input length, which rules out the constructs that make a regex able to blow up exponentially. A pattern that takes a hostile input and hangs the process is not possible here.
_, err := regexp.Compile(`(\w)\1`)
fmt.Println(err)
// output: error parsing regexp: invalid escape sequence: `\1`
_, err = regexp.Compile(`(?=foo)`)
fmt.Println(err)
// output: error parsing regexp: invalid or unsupported Perl syntax: `(?=`
If you need them, the answer is usually to match something broader and then check the rest in ordinary Go code — which is clearer than the lookahead would have been anyway.
Escaping a literal¶
When part of a pattern comes from data, escape it:
Reach for strings first¶
A regex is slower and harder to read than a direct call. If
strings.Contains, HasPrefix, Cut or Fields will do the job, use
those — see strings, bytes and runes.
From Python:
MustCompileisre.compileat import time,MatchStringisre.searchreturning a bool, andFindStringSubmatchism.groups()with the full match prepended. The differences that will catch you: flags live inside the pattern, there is no match object so you check fornil, named groups needSubexpIndex, and\1and(?=...)simply do not exist.
Quick reference¶
| Task | Call |
|---|---|
| compile a constant pattern | regexp.MustCompile(`...`) at package level |
| compile untrusted input | regexp.Compile, handle the error |
| yes/no | MatchString |
| capture groups | FindStringSubmatch — nil means no match |
| named group | SubexpIndex("name") |
| every match | FindAllString(s, -1) |
| replace with groups | ReplaceAllString(s, "${1}") |
| replace with logic | ReplaceAllStringFunc |
| split | Split(s, -1) |
| case-insensitive / multiline / dotall | (?i) / (?m) / (?s) |
| escape literal text | regexp.QuoteMeta |