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:

GO
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))
	}
}
Output
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

PatternMatches
.Any character except newline
\d \w \sA digit; a word character (letter, digit, underscore); whitespace
\D \W \SThe 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
^ $ \bStart of text, end of text, word boundary
a|bEither 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

GO
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"))
}
Output
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:

GO
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)
}
Output
["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:

GO
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)
}
Output
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

GO
package main

import (
	"fmt"
	"regexp"
)

func main() {
	sep := regexp.MustCompile(`\s*[,;]\s*`)
	fmt.Printf("%q\n", sep.Split("red , green;blue ;  yellow", -1))
}
Output
["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.

Try it yourself · main.go
Edit the code, copy it into main.go in a new folder and run it with go run main.go (Go 1.27 or later). How to install Go
Output
Go
golang
100DaysOfCode
go
Next: fixed-size arrays.