Regular Expressions
A regular expression describes a pattern of text — "three digits, a dash, four digits" — so you can test whether a string matches it, find matches, pull out parts, or replace them. Go's regexp package uses the RE2 syntax, which is guaranteed to run in time proportional to the input, so a pattern can never hang your program. This lesson covers compiling patterns, matching, finding, capturing groups and replacing.
Compiling and matching
Compile a pattern once with regexp.MustCompile and reuse it. Write patterns as raw strings in backticks so that backslashes don't need escaping:
package main import ( "fmt" "regexp" ) var postcode = regexp.MustCompile(`^[A-Z]{2}\d{1,2} \d[A-Z]{2}$`) func main() { for _, s := range []string{"SW1 2AB", "EC12 9ZZ", "sw1 2ab", "SW1 2AB"} { fmt.Println(s, postcode.MatchString(s)) } }
SW1 2AB true EC12 9ZZ true sw1 2ab false SW1 2AB false
MustCompile panics if the pattern is invalid, which suits patterns written into your code (usually as package-level variables, compiled once at startup). For patterns that come from users, use regexp.Compile, which returns an error instead.
Pattern syntax
| Pattern | Matches |
|---|---|
. | Any character except newline |
\d \w \s | A digit; a word character (letter, digit, underscore); whitespace |
\D \W \S | The opposites of the above |
[abc] [a-z] [^0-9] | One character from a set, a range, or not in a set |
* + ? | Zero or more, one or more, zero or one of the previous item |
{3} {2,5} {2,} | Exactly 3, between 2 and 5, at least 2 |
*? +? | Lazy versions: match as little as possible |
^ $ \b | Start of text, end of text, word boundary |
a|b | Either alternative |
(…) (?P<name>…) (?:…) | Capturing group, named group, non-capturing group |
(?i) | Flag: case-insensitive from here on |
RE2 leaves out backreferences (\1) and lookarounds ((?=…)), the features that can make other regex engines exponentially slow.
Finding matches
package main import ( "fmt" "regexp" ) func main() { text := "Call 555-0142 or 555-0199 before 6pm. Fax: 555-0100." phone := regexp.MustCompile(`\d{3}-\d{4}`) fmt.Println(phone.FindString(text)) fmt.Println(phone.FindAllString(text, -1)) fmt.Println(phone.FindAllString(text, 2)) fmt.Println(phone.FindStringIndex(text)) word := regexp.MustCompile(`(?i)\bcall\b`) fmt.Println(word.MatchString("CALL me"), word.MatchString("recall")) }
555-0142 [555-0142 555-0199 555-0100] [555-0142 555-0199] [5 13] true false
The All variants take a limit; -1 means "no limit". FindStringIndex returns the start and end byte positions of the match.
Capturing groups
Parentheses capture parts of a match. FindStringSubmatch returns the whole match followed by each group. Named groups make the code easier to read:
package main import ( "fmt" "regexp" ) var logLine = regexp.MustCompile(`^(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>[A-Z]+) (?P<msg>.*)$`) func main() { m := logLine.FindStringSubmatch("2026-10-08 ERROR disk almost full") fmt.Printf("%q\n", m) date := m[logLine.SubexpIndex("date")] level := m[logLine.SubexpIndex("level")] fmt.Println(date, level) fmt.Println(logLine.FindStringSubmatch("not a log line") == nil) }
["2026-10-08 ERROR disk almost full" "2026-10-08" "ERROR" "disk almost full"] 2026-10-08 ERROR true
When there's no match, the result is nil, so check before indexing into it. FindAllStringSubmatch returns the groups for every match.
Replacing
In the replacement string, $1 or ${name} inserts a captured group. ReplaceAllStringFunc computes each replacement with a function:
package main import ( "fmt" "regexp" "strings" ) func main() { date := regexp.MustCompile(`(\d{2})/(\d{2})/(\d{4})`) fmt.Println(date.ReplaceAllString("Due 31/12/2026, paid 02/01/2027", "$3-$2-$1")) spaces := regexp.MustCompile(`\s+`) fmt.Println(spaces.ReplaceAllString("too many \t spaces", " ")) email := regexp.MustCompile(`[\w.+-]+@[\w-]+\.[\w.]+`) masked := email.ReplaceAllStringFunc("Contact ana@example.com or bo@test.org", func(s string) string { user, domain, _ := strings.Cut(s, "@") return user[:1] + "***@" + domain }) fmt.Println(masked) }
Due 2026-12-31, paid 2027-01-02 too many spaces Contact a***@example.com or b***@test.org
When a group number is directly followed by letters or digits, use braces — ${1}x — because $1x would be read as a group named "1x".
Splitting
package main import ( "fmt" "regexp" ) func main() { sep := regexp.MustCompile(`\s*[,;]\s*`) fmt.Printf("%q\n", sep.Split("red , green;blue ; yellow", -1)) }
["red" "green" "blue" "yellow"]
When not to use a regex
Regular expressions are powerful but hard to read, and slower than plain string functions. If strings.Contains, HasPrefix, Cut or Fields can do the job, prefer them. And don't use regexes to parse HTML, JSON or email addresses in full — use a proper parser (encoding/json, net/mail, net/url).
Try it yourself
Extract every hashtag from a post, then make the matching case-insensitive and remove duplicates.
Go golang 100DaysOfCode go