Regex #
The regexp package in Go implements RE2 syntax — not PCRE (Perl Compatible Regular Expressions) used by Python, JavaScript, or PHP. The most important difference: RE2 doesn’t support lookahead, lookbehind, or backreferences. This isn’t an arbitrary limitation — RE2 guarantees linear execution time relative to input length, preventing ReDoS (Regular Expression Denial of Service). For most validation and text extraction needs, RE2 is more than sufficient. This article covers everything you need to know to use regex effectively in Go.
The general workflow of using regular expressions in Go consists of a text pattern compilation stage (regex compiling) followed by a text matching stage (matching/searching), as visualized in the following diagram:
flowchart TD
Pattern["Pattern String (Literal)"] --> CompilePhase{"Compilation Type?"}
CompilePhase -->|"Dynamic (User Input)"| Compile["regexp.Compile(pattern)"]
CompilePhase -->|"Static (Global Var)"| MustCompile["regexp.MustCompile(pattern)"]
Compile -->|"Passes"| RegexObj["Regexp Object (*regexp.Regexp)"]
Compile -->|"Fails"| Err["Return Error"]
MustCompile -->|"Passes"| RegexObj
MustCompile -->|"Fails"| Panic["Panic (Program Stops)"]
RegexObj --> Match["Check/Match Operation:\nMatchString() / FindString() / ReplaceAll()"]Compile vs MustCompile
#
There are two ways to create a *regexp.Regexp object:
import "regexp"
// regexp.Compile — returns an error if the pattern is invalid
re, err := regexp.Compile(`\d{4}-\d{2}-\d{2}`)
if err != nil {
log.Fatal("Invalid pattern:", err)
}
// regexp.MustCompile — panics if the pattern is invalid
// Used for patterns known to be valid at coding time
re2 := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
When to Use Each #
// USE MustCompile for literal patterns at the package level
// The pattern is evaluated once when the program starts — a panic is a programmer bug, not a user error
var (
emailRegex = regexp.MustCompile(
`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
phoneRegex = regexp.MustCompile(`^(\+62|62|0)8[1-9][0-9]{6,9}$`)
dateRegex = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$`)
uuidRegex = regexp.MustCompile(
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
)
// USE Compile for patterns coming from user input or configuration
func validateWithCustomPattern(input, pattern string) (bool, error) {
re, err := regexp.Compile(pattern)
if err != nil {
return false, fmt.Errorf("invalid regex pattern: %w", err)
}
return re.MatchString(input), nil
}
Always compile a regex once, use it many times. The regex compilation process (parsing, building the finite automaton) is fairly expensive. If you compile a regex inside a loop or inside a frequently-called function, performance will be very poor. Declare it as a package-levelvarwithMustCompile.
Raw String Literals — The Right Way to Write Patterns #
Regex often contains many backslashes. In regular strings, backslashes must be escaped (\\), making them hard to read. Use raw string literals (backticks) for regex:
// Regular string — backslashes must be escaped, hard to read
re1 := regexp.MustCompile("\\d{4}-\\d{2}-\\d{2}")
re1b := regexp.MustCompile("^[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}$")
// Raw string — easier to read
re2 := regexp.MustCompile(`\d{4}-\d{2}-\d{2}`)
re2b := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
Always use backticks for regex — there’s no sensible exception.
Basic Operations #
MatchString — Is There a Match?
#
re := regexp.MustCompile(`\d+`)
fmt.Println(re.MatchString("abc123")) // true — there are digits
fmt.Println(re.MatchString("abc")) // false — no digits
fmt.Println(re.MatchString("")) // false
// Match on []byte
fmt.Println(re.Match([]byte("abc123"))) // true
FindString — The First Match
#
re := regexp.MustCompile(`\d+`)
fmt.Println(re.FindString("abc123def456")) // "123" — the first match
fmt.Println(re.FindString("abcdef")) // "" — no match
// FindString returns "" if there's no match
// Use FindStringIndex to distinguish "no match" from "empty string matches"
FindAllString — All Matches
#
The second parameter is a limit: -1 for all matches, n for a maximum of n matches:
re := regexp.MustCompile(`\d+`)
text := "price: 15000, discount: 2000, total: 13000"
// All matches
all := re.FindAllString(text, -1)
fmt.Println(all) // [15000 2000 13000]
// Maximum 2 matches
two := re.FindAllString(text, 2)
fmt.Println(two) // [15000 2000]
FindStringIndex — Match Positions
#
re := regexp.MustCompile(`\d+`)
text := "abc123def456"
idx := re.FindStringIndex(text)
fmt.Println(idx) // [3 6] — the "123" match at indexes 3 to 5 (not including 6)
if idx != nil {
fmt.Println(text[idx[0]:idx[1]]) // "123"
}
allIdx := re.FindAllStringIndex(text, -1)
fmt.Println(allIdx) // [[3 6] [9 12]]
Capturing Groups #
Capturing groups with () let you extract specific parts of a match:
// FindStringSubmatch — [full_match, group1, group2, ...]
re := regexp.MustCompile(`(\d{4})-(\d{2})-(\d{2})`)
text := "Birth date: 2024-07-28"
match := re.FindStringSubmatch(text)
fmt.Println(match) // [2024-07-28 2024 07 28]
// match[0] = full match
// match[1] = year (group 1)
// match[2] = month (group 2)
// match[3] = day (group 3)
if match != nil {
fmt.Printf("Year: %s, Month: %s, Day: %s\n",
match[1], match[2], match[3])
}
// FindAllStringSubmatch — all matches with groups
re2 := regexp.MustCompile(`(\w+)=(\w+)`)
config := "host=localhost port=5432 db=myapp"
allMatches := re2.FindAllStringSubmatch(config, -1)
for _, m := range allMatches {
fmt.Printf("key=%s, value=%s\n", m[1], m[2])
}
// key=host, value=localhost
// key=port, value=5432
// key=db, value=myapp
Named Groups — (?P<name>...)
#
Named groups let you access results by name rather than index:
re := regexp.MustCompile(
`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
text := "2024-07-28"
match := re.FindStringSubmatch(text)
// Get the group names
names := re.SubexpNames()
fmt.Println(names) // ["" "year" "month" "day"]
// Build a map from names to values
result := make(map[string]string)
for i, name := range names {
if i != 0 && name != "" && i < len(match) {
result[name] = match[i]
}
}
fmt.Println(result["year"]) // 2024
fmt.Println(result["month"]) // 07
fmt.Println(result["day"]) // 28
// A more elegant helper function
func namedGroups(re *regexp.Regexp, s string) map[string]string {
match := re.FindStringSubmatch(s)
if match == nil {
return nil
}
result := make(map[string]string)
for i, name := range re.SubexpNames() {
if i != 0 && name != "" {
result[name] = match[i]
}
}
return result
}
Replacement #
ReplaceAllString — Replace with a String Literal
#
re := regexp.MustCompile(`\d+`)
text := "Price: 15000, discount: 2000"
// Replace all numbers with "X"
fmt.Println(re.ReplaceAllString(text, "X"))
// "Price: X, discount: X"
// Use $1, $2, ... for backreferences to captured groups
re2 := regexp.MustCompile(`(\w+)@(\w+)\.(\w+)`)
email := "[email protected]"
masked := re2.ReplaceAllString(email, "$1@***.$3")
fmt.Println(masked) // "budi@***.com"
ReplaceAllLiteralString — Replace Without Backreferences
#
re := regexp.MustCompile(`\$\d+`)
text := "Price: $100 and $200"
// With ReplaceAllString, $ is interpreted as a group reference
// Use ReplaceAllLiteralString if the replacement contains $
fmt.Println(re.ReplaceAllLiteralString(text, "USD"))
// "Price: USD and USD"
ReplaceAllStringFunc — Replace with a Function
#
This is the most powerful feature — you can transform every match programmatically:
// Convert all words to title case
re := regexp.MustCompile(`\b\w+\b`)
text := "the quick brown fox"
result := re.ReplaceAllStringFunc(text, func(match string) string {
if len(match) == 0 {
return match
}
return strings.ToUpper(match[:1]) + match[1:]
})
fmt.Println(result) // "The Quick Brown Fox"
// Email masking — show only the first 2 characters
emailRe := regexp.MustCompile(`\b[\w.]+@[\w.]+\b`)
logLine := "User [email protected] logged in from IP 192.168.1.1"
masked := emailRe.ReplaceAllStringFunc(logLine, func(email string) string {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return email
}
user := parts[0]
if len(user) > 2 {
user = user[:2] + strings.Repeat("*", len(user)-2)
}
return user + "@" + parts[1]
})
fmt.Println(masked) // "User bu**********@gmail.com logged in from IP 192.168.1.1"
Splitting #
re := regexp.MustCompile(`[\s,;]+`) // split on spaces, commas, or semicolons
text := "apple, mango; orange durian"
parts := re.Split(text, -1)
fmt.Println(parts) // [apple mango orange durian]
// SplitN — maximum n parts
parts2 := re.Split(text, 3)
fmt.Println(parts2) // [apple mango orange durian] — only 2 splits
Flags #
Flags change matching behavior. In RE2, flags are written inside the pattern:
// (?i) — case insensitive
re := regexp.MustCompile(`(?i)golang`)
fmt.Println(re.MatchString("Golang")) // true
fmt.Println(re.MatchString("GOLANG")) // true
fmt.Println(re.MatchString("gOlAnG")) // true
// (?m) — multiline: ^ and $ match at the start/end of each line
re2 := regexp.MustCompile(`(?m)^\d+`)
text := "123 abc\n456 def\n789 ghi"
fmt.Println(re2.FindAllString(text, -1)) // [123 456 789]
// (?s) — dot-all: . also matches newlines
re3 := regexp.MustCompile(`(?s)start.+end`)
fmt.Println(re3.MatchString("start\nmiddle\nend")) // true
// (?im) — a combination of flags
re4 := regexp.MustCompile(`(?im)^hello`)
// Flags apply to the whole pattern unless placed in a group
// (?i:kata) — the flag only applies to this group
re5 := regexp.MustCompile(`(?i:go)lang`)
fmt.Println(re5.MatchString("Golang")) // true — "Go" is case-insensitive
fmt.Println(re5.MatchString("golANG")) // false — "lang" is still case-sensitive
Important RE2 Syntax #
ANCHORS:
^ Start of string (or line with the (?m) flag)
$ End of string (or line with the (?m) flag)
\A Start of string (unaffected by (?m))
\z End of string (unaffected by (?m))
CHARACTER CLASSES:
. Any character except newline (with (?s), includes newlines)
\d Digit [0-9]
\D Non-digit
\w Word character [0-9A-Za-z_]
\W Non-word character
\s Whitespace [ \t\n\f\r]
\S Non-whitespace
[abc] Character class: a, b, or c
[^abc] Negated: not a, b, or c
[a-z] Range: a to z
[a-zA-Z0-9] Alphanumeric
QUANTIFIERS:
* 0 or more (greedy)
+ 1 or more (greedy)
? 0 or 1 (greedy)
*? 0 or more (lazy/non-greedy)
+? 1 or more (lazy/non-greedy)
?? 0 or 1 (lazy)
{n} Exactly n
{n,} n or more
{n,m} Between n and m
GROUPS:
(abc) Capturing group
(?:abc) Non-capturing group
(?P<name>abc) Named capturing group
(a|b) Alternation: a or b
NOT IN RE2 (different from PCRE):
(?=...) Lookahead — NOT SUPPORTED
(?!...) Negative lookahead — NOT SUPPORTED
(?<=...) Lookbehind — NOT SUPPORTED
\1, \2 Backreference — NOT SUPPORTED
Common Regex Patterns for Validation #
var (
// Email — simple but covers most cases
EmailRegex = regexp.MustCompile(
`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
// Indonesian phone number
PhoneIDRegex = regexp.MustCompile(
`^(\+62|62|0)(8[1-9])[0-9]{6,9}$`)
// URL (simple)
URLRegex = regexp.MustCompile(
`^https?://[^\s/$.?#].[^\s]*$`)
// ISO 8601 date (YYYY-MM-DD)
DateRegex = regexp.MustCompile(
`^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$`)
// IPv4 Address
IPv4Regex = regexp.MustCompile(
`^(25[0-5]|2[0-4]\d|[01]?\d\d?)(\.(25[0-5]|2[0-4]\d|[01]?\d\d?)){3}$`)
// UUID v4
UUIDRegex = regexp.MustCompile(
`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
// Username: letters, digits, underscores, 3-20 characters
UsernameRegex = regexp.MustCompile(`^[a-zA-Z0-9_]{3,20}$`)
// Password: min 8 chars, has uppercase, lowercase, digits
// (complex validation is better done with Go logic than regex)
// Indonesian postal code (5 digits)
PostalCodeIDRegex = regexp.MustCompile(`^\d{5}$`)
// NIK KTP (16 digits)
NIKRegex = regexp.MustCompile(`^\d{16}$`)
// Indonesian vehicle license plate (simple)
LicensePlateRegex = regexp.MustCompile(
`^[A-Z]{1,2}\s?\d{1,4}\s?[A-Z]{1,3}$`)
)
When NOT to Use Regex #
Regex isn’t the solution to every string problem. For simple operations, the strings package is much faster and easier to read:
// ANTI-PATTERN: regex for simple string operations
hasPrefix := regexp.MustCompile(`^Hello`).MatchString(text)
hasSuffix := regexp.MustCompile(`World$`).MatchString(text)
contains := regexp.MustCompile(`golang`).MatchString(text)
// CORRECT: use the strings package
hasPrefix2 := strings.HasPrefix(text, "Hello")
hasSuffix2 := strings.HasSuffix(text, "World")
contains2 := strings.Contains(text, "golang")
// For simple splits
parts := regexp.MustCompile(`\s+`).Split(text, -1)
// Better:
parts2 := strings.Fields(text) // split on whitespace
// For simple replacements
result := regexp.MustCompile(`old`).ReplaceAllString(text, "new")
// Better:
result2 := strings.ReplaceAll(text, "old", "new")
Use regex when:
✓ The pattern is dynamic or complex
✓ You need capturing groups for extraction
✓ Validation rules that can't be expressed with strings
✓ Parsing unstructured text
Use the strings package when:
✓ Checking prefix/suffix — strings.HasPrefix/HasSuffix
✓ Checking substring existence — strings.Contains
✓ Splitting on fixed delimiters — strings.Split
✓ Replacing string literals — strings.ReplaceAll
✓ Case conversion — strings.ToUpper/ToLower
Complete Example Program #
The following program builds a form validator using various regex patterns:
package main
import (
"fmt"
"regexp"
"strings"
)
// ── Regex Patterns ────────────────────────────────────────────
var (
reEmail = regexp.MustCompile(
`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
rePhone = regexp.MustCompile(
`^(\+62|62|0)(8[1-9])[0-9]{6,9}$`)
reUsername = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_]{2,19}$`)
reURL = regexp.MustCompile(
`^https?://[a-zA-Z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$`)
reDate = regexp.MustCompile(
`^(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$`)
reHTMLTag = regexp.MustCompile(`<[^>]+>`)
reWhitespace = regexp.MustCompile(`\s+`)
reNumber = regexp.MustCompile(`\b\d+(?:\.\d+)?\b`)
reHashtag = regexp.MustCompile(`#(\w+)`)
reMention = regexp.MustCompile(`@(\w+)`)
)
// ── Validation Result ─────────────────────────────────────────
type ValidationResult struct {
Field string
Value string
Valid bool
Message string
}
func validate(field, value string, re *regexp.Regexp, msg string) ValidationResult {
valid := re.MatchString(value)
result := ValidationResult{Field: field, Value: value, Valid: valid}
if !valid {
result.Message = msg
}
return result
}
// ── Text Processing ───────────────────────────────────────────
// StripHTML removes all HTML tags from text
func stripHTML(html string) string {
return reHTMLTag.ReplaceAllString(html, "")
}
// NormalizeWhitespace replaces repeated whitespace with a single space
func normalizeWhitespace(s string) string {
return strings.TrimSpace(reWhitespace.ReplaceAllString(s, " "))
}
// ExtractNumbers extracts all numbers from text
func extractNumbers(text string) []string {
return reNumber.FindAllString(text, -1)
}
// ExtractHashtags extracts all hashtags from text
func extractHashtags(text string) []string {
matches := reHashtag.FindAllStringSubmatch(text, -1)
tags := make([]string, 0, len(matches))
for _, m := range matches {
tags = append(tags, m[1]) // take group 1, not the full match
}
return tags
}
// ExtractMentions extracts all mentions from text
func extractMentions(text string) []string {
matches := reMention.FindAllStringSubmatch(text, -1)
mentions := make([]string, 0, len(matches))
for _, m := range matches {
mentions = append(mentions, m[1])
}
return mentions
}
// ParseDate extracts date components
func parseDate(s string) (year, month, day string, ok bool) {
m := reDate.FindStringSubmatch(s)
if m == nil {
return "", "", "", false
}
return m[1], m[2], m[3], true
}
// MaskEmail masks part of an email
func maskEmail(email string) string {
reMask := regexp.MustCompile(`^(.{2})(.+)(@.+)$`)
return reMask.ReplaceAllStringFunc(email, func(s string) string {
m := reMask.FindStringSubmatch(s)
if m == nil {
return s
}
hidden := strings.Repeat("*", len(m[2]))
return m[1] + hidden + m[3]
})
}
// ── Main ──────────────────────────────────────────────────────
func main() {
fmt.Println("=== Form Validator ===\n")
// Form data to validate
testCases := []struct {
field string
value string
re *regexp.Regexp
msg string
}{
{"email", "[email protected]", reEmail, "Invalid email format"},
{"email", "not-an-email", reEmail, "Invalid email format"},
{"email", "user@domain", reEmail, "Invalid email format"},
{"phone", "+628****7890", rePhone, "Invalid phone number"},
{"phone", "08123456789", rePhone, "Invalid phone number"},
{"phone", "123456", rePhone, "Invalid phone number"},
{"username", "budi_santoso", reUsername, "Username: 3-20 chars, letters/digits/underscores"},
{"username", "b", reUsername, "Username: 3-20 chars, letters/digits/underscores"},
{"username", "1invalid", reUsername, "Username must start with a letter"},
{"url", "https://www.google.com", reURL, "Invalid URL"},
{"url", "ftp://server.com", reURL, "URL must start with http/https"},
{"date", "2024-07-28", reDate, "Date format: YYYY-MM-DD"},
{"date", "2024-13-01", reDate, "Invalid month"},
{"date", "28-07-2024", reDate, "Date format: YYYY-MM-DD"},
}
for _, tc := range testCases {
r := validate(tc.field, tc.value, tc.re, tc.msg)
status := "✓"
detail := ""
if !r.Valid {
status = "✗"
detail = " → " + r.Message
}
fmt.Printf(" [%s] %-10s: %q%s\n", status, r.Field, r.Value, detail)
}
fmt.Println("\n=== Text Processing ===\n")
// HTML stripping
html := `<h1>Title</h1><p>This is <strong>text</strong> with <em>formatting</em>.</p>`
fmt.Printf("HTML : %s\n", html)
fmt.Printf("Stripped: %s\n\n", stripHTML(html))
// Whitespace normalization
messy := " Text with lots of spaces "
fmt.Printf("Messy : %q\n", messy)
fmt.Printf("Normalized: %q\n\n", normalizeWhitespace(messy))
// Number extraction
receipt := "Bought 3 apples @ Rp5000, total Rp15000 + tax Rp1500 = Rp16500"
numbers := extractNumbers(receipt)
fmt.Printf("Text : %s\n", receipt)
fmt.Printf("Numbers: %v\n\n", numbers)
// Hashtag and mention extraction from social media text
post := "Just learning #golang and #programming! Thanks @gopher and @godev for the tutorials!"
hashtags := extractHashtags(post)
mentions := extractMentions(post)
fmt.Printf("Post : %s\n", post)
fmt.Printf("Hashtags : %v\n", hashtags)
fmt.Printf("Mentions : %v\n\n", mentions)
// Date parsing
dateStr := "2024-07-28"
if year, month, day, ok := parseDate(dateStr); ok {
fmt.Printf("Parse %q: year=%s, month=%s, day=%s\n",
dateStr, year, month, day)
}
// Email masking
emails := []string{
"[email protected]",
"[email protected]",
"[email protected]",
}
fmt.Println("\nEmail masking:")
for _, e := range emails {
fmt.Printf(" %-35s → %s\n", e, maskEmail(e))
}
// Demonstrate ReplaceAllStringFunc — format numbers with thousands separators
fmt.Println("\nNumber formatting:")
reRupiahValue := regexp.MustCompile(`\d+`)
prices := "price: 15000000, discount: 500000, total: 14500000"
formatted := reRupiahValue.ReplaceAllStringFunc(prices, func(s string) string {
n := len(s)
if n <= 3 {
return s
}
var result strings.Builder
for i, c := range s {
if i > 0 && (n-i)%3 == 0 {
result.WriteByte('.')
}
result.WriteRune(c)
}
return result.String()
})
fmt.Printf(" %s\n", formatted)
}
Summary #
- Go uses RE2, not PCRE — no lookahead, lookbehind, or backreferences; but linear time is guaranteed (safe from ReDoS).
MustCompilefor literal patterns at the package level — a panic at startup is better than a panic at runtime due to a programmer bug.Compilefor patterns from external input — always handle its error.- Always use raw string literals (backticks) for regex — avoids double-escaping backslashes.
- Compile once, use many times — declare as a package-level
var, not inside frequently-called functions.FindAllString(text, -1)—-1means all matches; usento limit the count.FindStringSubmatchreturns[full_match, group1, group2, ...]; index 0 is always the full match.- Named groups
(?P<name>...)andSubexpNames()for access by name rather than index.ReplaceAllStringFuncfor programmatic transformation of every match — very flexible.- Don’t use regex for simple string operations —
strings.Contains,HasPrefix,Split,ReplaceAllare much faster and easier to read.