Regular Expressions (regexp)
Reviewed & published by Brayan K
The regexp package matches, finds, extracts, and replaces text using patterns — you compile a pattern once with regexp.MustCompile and reuse the resulting matcher everywhere.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Compile patterns with regexp.MustCompile
- Test text with MatchString
- Find the first and all matches
- Extract groups with FindStringSubmatch
- Use named groups for clarity
- Replace text with ReplaceAllString
1️⃣ Compile, Match & Find
Start by compiling a pattern with regexp.MustCompile — written as a backtick raw string so the backslashes stay literal. The resulting *Regexp gives you MatchString (does it match at all?), FindString (the first match), and FindAllString (every match; pass -1 for no limit). Compile once and reuse — it's safe across goroutines.
package main
import (
"fmt"
"regexp"
)
func main() {
// MustCompile compiles the pattern once and panics on a bad pattern.
// Compile at package level / startup, not inside hot loops.
re := regexp.MustCompile(`\d+`) // one or more digits
fmt.Println(re.MatchString("abc123")) // true
fmt.Println(re.MatchString("abc")) // false
// FindString returns the FIRST match (or "" if none).
fmt.Println(re.FindString("order 42, item 7")) // 42
// FindAllString returns ALL matches (the -1 means "no limit").
fmt.Println(re.FindAllString("order 42, item 7", -1)) // [42 7]
}2️⃣ Capture Groups & Named Groups
Wrap part of a pattern in parentheses to make a capture group. FindStringSubmatch then returns a slice where index 0 is the whole match and 1, 2, 3... are the groups in order. For readability, use named groups (?P<name>...) and look up a name's position with SubexpIndex — far clearer than counting parentheses.
package main
import (
"fmt"
"regexp"
)
func main() {
// Capture groups in (...) let you pull out parts of a match.
re := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
// FindStringSubmatch returns [whole, group1, group2, ...].
m := re.FindStringSubmatch("contact ada@example.com today")
fmt.Println("whole:", m[0]) // ada@example.com
fmt.Println("user: ", m[1]) // ada
fmt.Println("host: ", m[2]) // example
fmt.Println("tld: ", m[3]) // com
// NAMED groups (?P<name>...) are clearer for complex patterns.
dre := regexp.MustCompile(`(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})`)
dm := dre.FindStringSubmatch("date 2026-06-18")
idx := dre.SubexpIndex("m")
fmt.Println("month:", dm[idx]) // 06
}3️⃣ Replacing & Validating
ReplaceAllString swaps every match for a replacement, and in that replacement $1, $2 (or ${name}) reference capture groups — perfect for reordering or reformatting. To validate a whole string, anchor the pattern with ^ and $ and call MatchString, so the pattern must cover the entire input.
package main
import (
"fmt"
"regexp"
)
func main() {
// ReplaceAllString swaps every match for a replacement string.
re := regexp.MustCompile(`\s+`) // runs of whitespace
fmt.Println(re.ReplaceAllString("too many spaces", " "))
// $1, $2 (or ${name}) reference capture groups in the replacement.
swap := regexp.MustCompile(`(\w+)\s(\w+)`)
fmt.Println(swap.ReplaceAllString("Ada Lovelace", "$2 $1"))
// Anchors ^ and $ pin the match to start/end; MatchString validates.
valid := regexp.MustCompile(`^[a-z]+\d+$`)
fmt.Println(valid.MatchString("abc123")) // true
fmt.Println(valid.MatchString("abc123!")) // false
}🎯 Your Turn
Find every hashtag — a # followed by one or more word characters. Fill in the blank in the pattern.
package main
import (
"fmt"
"regexp"
)
func main() {
// Find every hashtag: a # followed by one or more word characters.
re := regexp.MustCompile(`#___`) // 👉 \w+
tags := re.FindAllString("Loving #go and #golang today!", -1)
fmt.Println(tags)
// ✅ Expected output: [#go #golang]
}Common Errors (and the fix)
- ❌ Double-escaped pattern in a "..." string — hard to read and error-prone. ✅ Use a backtick raw string so \d stays literal.
- ❌ "panic: regexp: Compile..." — an invalid pattern hit MustCompile. ✅ Fix the pattern (or use Compile and handle the error).
- ❌ Indexed m[1] on a nil result — there was no match. ✅ Check if m != nil first.
- ❌ Recompiling inside a loop — slow. ✅ Compile once into a package-level var and reuse it.
🎯 Mini-Challenge: Parse Phone Numbers
Capture the area code and line number from every phone number with a pattern like (\d{3})-(\d{4}) (as a raw string). Use FindAllStringSubmatch and print each pair.
package main
import (
"fmt"
"regexp"
)
func main() {
// 🎯 MINI-CHALLENGE: pull area + line from every phone number.
// Pattern: three digits, a dash, four digits, with each part captured.
// Use FindAllStringSubmatch(in, -1) and print "area=... line=..." per match.
//
// ✅ Expected output:
// area=555 line=1234
// area=555 line=9876
in := "call 555-1234 or 555-9876"
// your code here (hint pattern: (\d{3})-(\d{4}) as a raw string)
}🎉 Lesson Complete!
- ✅ Compile once with regexp.MustCompile using a backtick raw string
- ✅ MatchString tests; FindString/FindAllString extract matches
- ✅ FindStringSubmatch returns the whole match plus capture groups
- ✅ Named groups (?P<name>...) + SubexpIndex keep patterns readable
- ✅ ReplaceAllString with $1/${name} reformats text
Practice quiz
Which function compiles a pattern and panics on an invalid one?
- regexp.Compile
- regexp.MustCompile
- regexp.New
- regexp.Build
Answer: regexp.MustCompile. regexp.MustCompile compiles the pattern and panics if it is invalid — ideal for package-level patterns.
Why are raw string literals (backticks) preferred for regex patterns?
- They run faster
- Backslashes need no escaping
- They allow newlines only
- They are required
Answer: Backslashes need no escaping. Raw strings avoid double-escaping backslashes, so `\d+` stays readable.
What does re.MatchString(s) return?
- The matched text
- A bool: whether the pattern matches somewhere in s
- The match index
- A slice of matches
Answer: A bool: whether the pattern matches somewhere in s. MatchString reports a bool indicating whether the regexp matches any part of s.
What does re.FindString(s) return when there is a match?
- true
- The leftmost matched substring
- The whole string
- A slice
Answer: The leftmost matched substring. FindString returns the text of the leftmost match.
What does re.FindString(s) return when there is no match?
- nil
- An empty string
- A panic
- false
Answer: An empty string. With no match, FindString returns the empty string "".
What does re.FindAllString(s, -1) return?
- The first match
- A slice of all non-overlapping matches
- A count
- A bool
Answer: A slice of all non-overlapping matches. FindAllString with n=-1 returns a []string of all matches.
What does re.FindStringSubmatch return for a pattern with groups?
- Only group 1
- The whole match plus each capture group
- A bool
- Only the whole match
Answer: The whole match plus each capture group. Index 0 is the full match; subsequent indexes are the capture groups.
How do you write a named capture group in Go's regexp?
- (<name>...)
- (?P<name>...)
- (?:name...)
- [name...]
Answer: (?P<name>...). Go uses RE2's (?P<name>...) syntax for named groups.
Which method gives the index of a named group for use after FindStringSubmatch?
- GroupIndex
- SubexpIndex
- NameIndex
- MatchIndex
Answer: SubexpIndex. re.SubexpIndex("name") returns the position of that named group in the submatch slice.
In ReplaceAllString, how do you reference the first capture group?
- \1
- $1
- %1
- {1}
Answer: $1. Use $1 (or ${1}) to reference the first numbered capture group in the replacement.
Continue this course
- Previous: Time & Dates (time package)
- Next: Checkpoint: Standard Library — Combine strings, sorting, maps and regexp in a build challenge — then a quiz
Frequently asked questions
Why do Go regex patterns use backtick raw strings?
Regex patterns are full of backslashes (\d, \w, \s). In a normal double-quoted Go string, the backslash is itself an escape character, so you'd have to double every one ("\\d"). A raw string literal written with backticks treats backslashes literally, so you can write the pattern exactly as a regex: `\d+`. That's why almost all Go regex code uses backtick raw strings.
What is the difference between Compile and MustCompile?
regexp.Compile returns (*Regexp, error) so you can handle an invalid pattern at run time. regexp.MustCompile returns just *Regexp and panics if the pattern is invalid. Because patterns are usually constant and known at development time, MustCompile is preferred — and you call it once at package level so a typo fails loudly at startup rather than deep in a request.
Why should I compile the regex once instead of in a loop?
Compiling a regular expression parses the pattern and builds a matching engine, which is relatively expensive. If you call MustCompile inside a hot loop you pay that cost on every iteration. Compile once — typically into a package-level var declared with regexp.MustCompile — and reuse the *Regexp, which is safe for concurrent use by multiple goroutines.
How do I extract capture groups?
Use FindStringSubmatch, which returns a slice where index 0 is the whole match and 1, 2, 3... are the parenthesized groups in order. For many matches, FindAllStringSubmatch returns a slice of those slices. Named groups written as (?P<name>...) are clearer for complex patterns; find a named group's position with re.SubexpIndex("name").
How do I reference capture groups in a replacement?
In ReplaceAllString, use $1, $2, ... for numbered groups and ${name} for named ones. For example, swapping "first last" to "last first" is ReplaceAllString(s, "$2 $1"). If a group name could run into following text, wrap it in braces (${name}) so Go knows where the reference ends.