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-level var with MustCompile.

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).
  • MustCompile for literal patterns at the package level — a panic at startup is better than a panic at runtime due to a programmer bug.
  • Compile for 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)-1 means all matches; use n to limit the count.
  • FindStringSubmatch returns [full_match, group1, group2, ...]; index 0 is always the full match.
  • Named groups (?P<name>...) and SubexpNames() for access by name rather than index.
  • ReplaceAllStringFunc for programmatic transformation of every match — very flexible.
  • Don’t use regex for simple string operationsstrings.Contains, HasPrefix, Split, ReplaceAll are much faster and easier to read.

← Previous: Date & Time   Next: Vendoring →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact