58. `regexp`
Go's regular expressions use RE2, which guarantees linear-time matching — no catastrophic backtracking, ever. The price is that a few features you may know from Perl or JavaScript (backreferences, lookahead) simply don't exist. That's a trade Go makes on purpose.
Compile once, use many times
package main
import (
"fmt"
"regexp"
)
func main() {
re, err := regexp.Compile(`\d+`)
if err != nil {
fmt.Println("bad pattern:", err)
return
}
fmt.Println(re.MatchString("abc 123"))
fmt.Println(re.FindString("order 42 shipped"))
fmt.Println(re.FindAllString("1 apple, 22 pears, 333 figs", -1))
}
Compile returns (*Regexp, error) — a pattern is data, and data can be
malformed.
Note the backtick string: regexes are full of backslashes, and a raw
string literal means you write \d instead of "\\d".
MustCompile for fixed patterns
package main
import (
"fmt"
"regexp"
)
var emailRE = regexp.MustCompile(`^[\w.+-]+@[\w-]+\.[\w.]+$`)
func main() {
for _, s := range []string{"ada@example.com", "not-an-email", "grace@sub.example.co.uk"} {
fmt.Printf("%-25s %t\n", s, emailRE.MatchString(s))
}
}
MustCompile panics instead of returning an error — the Must convention
from module 7. Use it for hard-coded patterns in package-level variables,
because a bad literal regex is a bug that should stop the program at startup.
Compile once, at package level. Compiling inside a function that runs per request is a classic performance mistake: it's expensive, and the result is identical every time.
Finding, replacing, splitting
package main
import (
"fmt"
"regexp"
)
func main() {
text := "Contact: ada@example.com or grace@example.org for details."
re := regexp.MustCompile(`[\w.]+@[\w.]+\.\w+`)
fmt.Println(re.FindString(text))
fmt.Println(re.FindAllString(text, -1))
fmt.Println(re.FindAllString(text, 1))
fmt.Println(re.FindStringIndex(text))
fmt.Println(re.ReplaceAllString(text, "[redacted]"))
spaces := regexp.MustCompile(`\s+`)
fmt.Println(spaces.Split("split on any whitespace", -1))
fmt.Println(re.NumSubexp(), len(re.FindAllString(text, -1)))
}
The -1 argument means "all matches"; a positive number caps them. That
pattern is consistent across every FindAll... function.
The naming is systematic once you see it: Find + All? + String? +
Submatch? + Index?. FindAllStringSubmatchIndex is a real function, and
you can now read what it does.
Capture groups
Parentheses capture, and Submatch gives you what they caught:
package main
import (
"fmt"
"regexp"
)
func main() {
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
m := re.FindStringSubmatch("released on 2024-03-15 officially")
fmt.Println(len(m))
fmt.Println("whole match:", m[0])
fmt.Println("year:", m[1], "month:", m[2], "day:", m[3])
all := re.FindAllStringSubmatch("2024-03-15 and 2025-01-01", -1)
for _, match := range all {
fmt.Printf("%s -> year %s\n", match[0], match[1])
}
}
m[0] is always the entire match; m[1], m[2]... are the groups in order.
A non-participating group gives you an empty string, so check len(m) before
indexing.
Named groups
Positional indexes get unreadable fast. Name them:
package main
import (
"fmt"
"regexp"
)
var logRE = regexp.MustCompile(`^(?P<level>\w+)\s+(?P<time>[\d:]+)\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
}
func main() {
fields := parse("ERROR 14:30:05 database connection failed")
fmt.Println(fields["level"])
fmt.Println(fields["time"])
fmt.Println(fields["msg"])
fmt.Println(parse("not a log line") == nil)
}
(?P<name>...) is Go's syntax for a named group, and SubexpNames() returns
the names by index (with an empty string at position 0 for the whole match).
That loop turning a match into a map is boilerplate worth keeping around.
Replacement with references
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
re := regexp.MustCompile(`(\w+)@(\w+)\.com`)
text := "mail ada@example.com and grace@other.com"
fmt.Println(re.ReplaceAllString(text, "$1 AT $2 dot com"))
fmt.Println(re.ReplaceAllString(text, "${1}_user"))
upper := re.ReplaceAllStringFunc(text, func(m string) string {
return strings.ToUpper(m)
})
fmt.Println(upper)
}
$1 refers to the first group. Use ${1} when a letter follows immediately,
or $1_user would be read as a group named 1_user and expand to nothing —
a small trap with a confusing failure mode.
ReplaceAllStringFunc gives you the matched text and lets you compute the
replacement, which covers everything $n can't.
What RE2 doesn't have
// These are NOT supported by Go's regexp:
(\w+)\s+\1 // backreferences
(?=foo) // lookahead
(?<=foo) // lookbehind
(?!foo) // negative lookahead
They're excluded because they're what make regex engines exponential. RE2 matches in time linear in the input length, always — so a hostile input can't hang your server, which is a real attack against naive regex use.
When you need those features, restructure: match a broader pattern and filter the results in Go code. It's usually clearer anyway.
When not to use a regex
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
s := "hello world"
fmt.Println(regexp.MustCompile(`^hello`).MatchString(s))
fmt.Println(strings.HasPrefix(s, "hello"))
fmt.Println(regexp.MustCompile(`world`).MatchString(s))
fmt.Println(strings.Contains(s, "world"))
fmt.Println(regexp.MustCompile(`\s`).Split(s, -1))
fmt.Println(strings.Fields(s))
}
Each pair does the same thing, and the strings version is an order of
magnitude faster and far easier to read. Reach for strings first —
Contains, HasPrefix, HasSuffix, Split, Fields, ReplaceAll cover
most real needs.
And for structured formats — HTML, JSON, CSV, email addresses to spec — use a
parser, not a regex. encoding/json, encoding/csv and net/mail exist for
exactly this.
Regexes earn their place on genuinely irregular text: log lines, free-form user input, ad-hoc extraction.
Your turn
Extract all the numbers from the string and print them:
[42 7 100]
package main
import (
"fmt"
"regexp"
)
func main() {
text := "order 42 has 7 items worth 100 dollars"
// find all runs of digits and print the slice
}
package main
import (
"fmt"
"regexp"
)
func main() {
text := "order 42 has 7 items worth 100 dollars"
re := regexp.MustCompile(`\d+`)
fmt.Println(re.FindAllString(text, -1))
}
That's the standard-library tour. Next: proving your code works.