Regexp #

Regular expressions are a very powerful tool for pattern matching, validation, and text extraction — but also one of the easiest to misuse. The regexp package in Go implements the RE2 syntax, which differs from PCRE (Perl-Compatible Regular Expressions) commonly used in other languages: RE2 guarantees linear execution time O(n) against the input length, meaning there’s no such thing as “catastrophic backtracking” that can hang a program when receiving malicious input. This makes Go regexes safe to use in production applications that accept user input. This article covers how to use regexp effectively — from compilation and basic matching, to capture groups, dynamic replacement, and when not to use regex at all.

An Overview of the regexp Package #

flowchart TD
    R["package regexp"] --> Compile["Regex Compilation"]
    R --> Match["Matching"]
    R --> Find["Search & Extraction"]
    R --> Replace["Replacement"]
    R --> Split["Splitting"]

    Compile --> C1["regexp.Compile(pattern)\nreturns *Regexp, error"]
    Compile --> C2["regexp.MustCompile(pattern)\npanics on error\nfor constant patterns"]
    Compile --> C3["regexp.CompilePOSIX\nPOSIX semantics"]

    Match --> M1["re.MatchString(s)\nbool — matches or not"]
    Match --> M2["re.Match([]byte)\nbool — for []byte"]

    Find --> F1["re.FindString\nthe first match"]
    Find --> F2["re.FindAllString\nall matches"]
    Find --> F3["re.FindStringSubmatch\nmatch + capture groups"]
    Find --> F4["re.FindAllStringSubmatch\nall + capture groups"]
    Find --> F5["re.FindStringIndex\nthe first match position"]

    Replace --> RP1["re.ReplaceAllString\nreplace all matches"]
    Replace --> RP2["re.ReplaceAllLiteralString\nwithout $ expansion"]
    Replace --> RP3["re.ReplaceAllStringFunc\nreplace with a function"]

    Split --> SP1["re.Split\nsplit by a pattern"]

    style R fill:#4f86c6,color:#fff
    style Compile fill:#e8f5e9
    style Match fill:#e3f2fd
    style Find fill:#fff3e0
    style Replace fill:#f3e5f5
    style Split fill:#fce4ec

Compiling Regexes #

Before it can be used, a regex pattern must be compiled into a *regexp.Regexp object. Compilation is expensive — always do it once, usually as a package variable:

package main

import (
    "fmt"
    "regexp"
)

// CORRECT: compile once as package variables (global level)
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}$`)
    reZipCode = regexp.MustCompile(`^\d{5}$`)
    reURLPath = regexp.MustCompile(`^/[a-zA-Z0-9/\-._~:@!$&'()*+,;=%?#]*$`)
)

// ANTI-PATTERN: compile inside a frequently called function
func validateEmailBad(email string) bool {
    re := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
    // re is compiled every time the function is called — very slow!
    return re.MatchString(email)
}

// CORRECT: use the already-compiled variable
func validateEmail(email string) bool {
    return reEmail.MatchString(email)
}

func main() {
    // Compile — returns an error if the pattern is invalid
    re, err := regexp.Compile(`(\w+)`)
    if err != nil {
        fmt.Println("invalid regex pattern:", err)
        return
    }
    fmt.Println(re.FindString("Hello World")) // Hello

    // MustCompile — panics if the pattern is invalid
    // Use ONLY for patterns you wrote yourself (not from user input!)
    re2 := regexp.MustCompile(`\d+`)
    fmt.Println(re2.FindString("abc 123 def")) // 123

    // DON'T: MustCompile with user input
    // userPattern := os.Stdin.ReadString('\n')
    // re3 := regexp.MustCompile(userPattern) // panics if the input is invalid!

    // CORRECT for user input: Compile and handle the error
    // re3, err := regexp.Compile(userPattern)
}

Go Regex Syntax (RE2) #

Go uses the RE2 syntax, not PCRE. The most important differences: no lookahead/lookbehind and no backreferences:

flowchart LR
    subgraph Basic["Basic Characters"]
        D1[". — any character except newline"]
        D2["\d — a digit [0-9]"]
        D3["\w — a word char [a-zA-Z0-9_]"]
        D4["\s — whitespace"]
        D5["\D \W \S — negations of the above"]
    end

    subgraph Quantifiers["Quantifiers"]
        K1["* — 0 or more"]
        K2["+ — 1 or more"]
        K3["? — 0 or 1"]
        K4["{n} — exactly n"]
        K5["{n,m} — n to m"]
        K6["{n,} — at least n"]
        K7["*? +? ?? — non-greedy"]
    end

    subgraph Anchors["Anchors"]
        A1["^ — start of the string (or line with (?m))"]
        A2["$ — end of the string (or line with (?m))"]
        A3["\b — word boundary"]
        A4["\A — start of the string (always)"]
        A5["\z — end of the string (always)"]
    end

    subgraph Groups["Groups & Alternation"]
        G1["(abc) — a capturing group"]
        G2["(?:abc) — a non-capturing group"]
        G3["(?P<name>abc) — a named capturing group"]
        G4["a|b — alternation: a or b"]
        G5["[abc] — a character set"]
        G6["[^abc] — a negated character set"]
    end

    subgraph Flags["Flags"]
        F1["(?i) — case-insensitive"]
        F2["(?m) — multiline (^ and $ match per line)"]
        F3["(?s) — . matches newlines too"]
        F4["(?U) — non-greedy by default"]
    end
// Regex syntax examples
re := regexp.MustCompile(`\d+`)           // one or more digits
re2 := regexp.MustCompile(`[a-z]+`)       // one or more lowercase letters
re3 := regexp.MustCompile(`(foo|bar)`)    // "foo" or "bar"
re4 := regexp.MustCompile(`\b\w+\b`)      // a whole word
re5 := regexp.MustCompile(`(?i)hello`)    // "hello" case-insensitive
re6 := regexp.MustCompile(`(?m)^\d+`)     // digits at the start of every line
re7 := regexp.MustCompile(`(?s).+`)       // all characters including newlines
re8 := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)

// Characters that need escaping in regex:
// . * + ? ^ $ {} [] | () \
// Use regexp.QuoteMeta for automatic escaping
userInput := "price $10.99 (discount)"
pattern := regexp.QuoteMeta(userInput) // "price \$10\.99 \(discount\)"
re9 := regexp.MustCompile(pattern)
fmt.Println(re9.MatchString(userInput)) // true

MatchString — Basic Matching #

var reEmail = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

// MatchString — does the string match the pattern?
fmt.Println(reEmail.MatchString("[email protected]"))  // true
fmt.Println(reEmail.MatchString("not-an-email"))      // false
fmt.Println(reEmail.MatchString("@nodomain.com"))     // false

// The top-level regexp.MatchString function — compiles on every call, avoid in production
matched, err := regexp.MatchString(`^\d+$`, "12345")
fmt.Println(matched, err) // true <nil>

// Match for []byte
data := []byte("Hello 123")
re := regexp.MustCompile(`\d+`)
fmt.Println(re.Match(data)) // true

FindString — Finding Matching Text #

re := regexp.MustCompile(`\d+`)
text := "I have 3 cats and 12 dogs"

// FindString — return the first match (an empty string if none)
fmt.Println(re.FindString(text)) // "3"

// FindAllString — return all matches
all := re.FindAllString(text, -1) // -1 = no limit
fmt.Println(all) // [3 12]

// Limit the number of matches
two := re.FindAllString(text, 2)
fmt.Println(two) // [3 12]

// FindStringIndex — the (start, end) position of the first match
idx := re.FindStringIndex(text)
fmt.Println(idx) // [11 12] — the position of "3"
fmt.Println(text[idx[0]:idx[1]]) // "3"

// FindAllStringIndex — the positions of all matches
all2 := re.FindAllStringIndex(text, -1)
fmt.Println(all2) // [[11 12] [22 24]]

// FindReaderIndex — read from an io.Reader
reader := strings.NewReader(text)
idxR := re.FindReaderIndex(reader)
fmt.Println(idxR) // [11 12]

Capture Groups — Extracting Specific Parts #

Capture groups (...) allow extracting specific parts of matching text:

flowchart LR
    Pattern["Pattern:<br/>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"] --> Input["Input: '2024-03-15'"]

    Input --> SM["FindStringSubmatch"]
    SM --> R0["[0] '2024-03-15'<br/>the whole match"]
    SM --> R1["[1] '2024'<br/>group 1: year"]
    SM --> R2["[2] '03'<br/>group 2: month"]
    SM --> R3["[3] '15'<br/>group 3: day"]

    SM --> Named["SubexpIndex('year')\n→ the named group index"]

    style Pattern fill:#4f86c6,color:#fff
    style R0 fill:#e8f5e9
    style R1 fill:#e3f2fd
    style R2 fill:#fff3e0
    style R3 fill:#f3e5f5
// A pattern with named capturing groups
reDate := regexp.MustCompile(
    `(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)

// FindStringSubmatch — returns [whole match, group1, group2, ...]
match := reDate.FindStringSubmatch("Date: 2024-03-15")
if match != nil {
    fmt.Println("Whole match:", match[0]) // "2024-03-15"
    fmt.Println("Year:", match[1])        // "2024"
    fmt.Println("Month:", match[2])       // "03"
    fmt.Println("Day:", match[3])         // "15"

    // Access named groups with SubexpIndex
    yearIdx := reDate.SubexpIndex("year")
    monthIdx := reDate.SubexpIndex("month")
    dayIdx := reDate.SubexpIndex("day")

    fmt.Printf("Year=%s, Month=%s, Day=%s\n",
        match[yearIdx], match[monthIdx], match[dayIdx])
}

// More idiomatic: build a map from named groups
func submatchMap(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
}

// Usage
fields := submatchMap(reDate, "2024-03-15")
fmt.Println(fields["year"])  // "2024"
fmt.Println(fields["month"]) // "03"
fmt.Println(fields["day"])   // "15"

// FindAllStringSubmatch — all matches with their groups
reNumbers := regexp.MustCompile(`(\d+)\.(\d+)`)
text := "Price: 15.000 and 250.000"
all := reNumbers.FindAllStringSubmatch(text, -1)
for _, m := range all {
    fmt.Printf("Match: %s, Parts: %s.%s\n", m[0], m[1], m[2])
}
// Match: 15.000, Parts: 15.000
// Match: 250.000, Parts: 250.000

ReplaceAll — Text Replacement #

re := regexp.MustCompile(`\d+`)

// ReplaceAllString — replace all matches with a fixed string
result := re.ReplaceAllString("abc 123 def 456", "NUM")
fmt.Println(result) // "abc NUM def NUM"

// ReplaceAllString with group references ($1, $2, ...)
reName := regexp.MustCompile(`(\w+)\s+(\w+)`)
result2 := reName.ReplaceAllString("Budi Santoso", "$2, $1")
fmt.Println(result2) // "Santoso, Budi"

// ReplaceAllLiteralString — doesn't process $ as a group reference
result3 := re.ReplaceAllLiteralString("abc 123 def", "$1")
fmt.Println(result3) // "abc $1 def" — $ isn't expanded

// ReplaceAllStringFunc — replace with the result of a function
result4 := re.ReplaceAllStringFunc("abc 123 def 456", func(match string) string {
    n, _ := strconv.Atoi(match)
    return strconv.Itoa(n * 2) // double every number
})
fmt.Println(result4) // "abc 246 def 912"

// Example: masking credit card numbers
reCreditCard := regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`)
logText := "Payment with card 4111 1111 1111 1111 successful"
safeText := reCreditCard.ReplaceAllStringFunc(logText, func(match string) string {
    // Only show the last 4 digits
    clean := regexp.MustCompile(`[\s-]`).ReplaceAllString(match, "")
    return "**** **** **** " + clean[len(clean)-4:]
})
fmt.Println(safeText) // "Payment with card **** **** **** 1111 successful"

// ReplaceAllFunc for []byte
re2 := regexp.MustCompile(`[aeiou]`)
result5 := re2.ReplaceAllFunc([]byte("hello world"), func(b []byte) []byte {
    return bytes.ToUpper(b)
})
fmt.Println(string(result5)) // "hEllO wOrld"

Split — Splitting Text #

// Split by a regex pattern
re := regexp.MustCompile(`[\s,;]+`) // whitespace, comma, or semicolon

text := "apple, mango; orange    durian"
fruits := re.Split(text, -1) // -1 = no limit
fmt.Println(fruits) // [apple mango orange durian]

// Limit the number of parts
parts := re.Split("a,b,c,d,e", 3)
fmt.Println(parts) // [a b c,d,e] — at most 3 parts

// Split with capture groups — the separator is included
reSeparator := regexp.MustCompile(`(\d+)`)
result := reSeparator.Split("abc123def456ghi", -1)
fmt.Println(result) // [abc def ghi] — the numbers (separators) aren't included

// To include separators, use FindAllStringIndex manually

Common Validation Patterns #

// A collection of ready-to-use validators
var (
    // Email — simple, not the complex RFC 5322
    ValidEmail = regexp.MustCompile(
        `^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

    // Indonesian phone numbers
    ValidPhone = regexp.MustCompile(
        `^(\+62|62|0)(811|812|813|821|822|823|851|852|853|` +
        `814|815|816|855|856|857|858|` +
        `817|818|819|859|877|878|` +
        `831|832|833|838|` +
        `895|896|897|898|899|` +
        `881|882|883|884|885|886|887|888|889)\d{5,8}$`)

    // Indonesian National ID (NIK) — 16 digits
    ValidNIK = regexp.MustCompile(`^\d{16}$`)

    // NPWP — format XX.XXX.XXX.X-XXX.XXX
    ValidNPWP = regexp.MustCompile(
        `^\d{2}\.\d{3}\.\d{3}\.\d-\d{3}\.\d{3}$`)

    // Indonesian postal codes
    ValidPostalCode = regexp.MustCompile(`^[1-9]\d{4}$`)

    // Vehicle license plates (simple)
    ValidPlate = regexp.MustCompile(
        `(?i)^[A-Z]{1,2}\s?\d{1,4}\s?[A-Z]{1,3}$`)

    // URL slugs
    ValidSlug = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)

    // UUID v4
    ValidUUID = regexp.MustCompile(
        `^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)

    // IPv4
    ValidIPv4 = regexp.MustCompile(
        `^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$`)

    // CSS hex colors (#RGB or #RRGGBB)
    ValidHexColor = regexp.MustCompile(`^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$`)
)

func validateInput(email, phone, nik string) []string {
    var errors []string

    if !ValidEmail.MatchString(email) {
        errors = append(errors, "invalid email format")
    }
    if !ValidPhone.MatchString(phone) {
        errors = append(errors, "invalid phone number")
    }
    if !ValidNIK.MatchString(nik) {
        errors = append(errors, "NIK must be 16 digits")
    }

    return errors
}

Common Parsing Patterns #

// Parse a log format: [2024-03-15 14:30:00] LEVEL message
reLog := regexp.MustCompile(
    `\[(?P<time>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+` +
    `(?P<level>\w+)\s+(?P<message>.+)`)

func parseLogLine(line string) (time, level, message string, ok bool) {
    m := reLog.FindStringSubmatch(line)
    if m == nil {
        return "", "", "", false
    }

    names := reLog.SubexpNames()
    for i, name := range names {
        switch name {
        case "time":
            time = m[i]
        case "level":
            level = m[i]
        case "message":
            message = m[i]
        }
    }
    return time, level, message, true
}

// Parse a simple URL
reURL := regexp.MustCompile(
    `(?P<scheme>https?)://(?P<host>[^/:]+)(?::(?P<port>\d+))?(?P<path>/[^?#]*)?(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?`)

// Parse a simple CSV (without quoting)
reCsv := regexp.MustCompile(`,\s*`)

func parseCSVLine(line string) []string {
    return reCsv.Split(line, -1)
}

// Extract all URLs from HTML
reLink := regexp.MustCompile(`href="([^"]+)"`)

func extractLinks(html string) []string {
    matches := reLink.FindAllStringSubmatch(html, -1)
    links := make([]string, 0, len(matches))
    for _, m := range matches {
        links = append(links, m[1]) // m[1] is the first capture group
    }
    return links
}

Performance: When Not to Use Regex #

flowchart TD
    Q{"What needs\nto be done?"} --> Simple["Simple operations:\ncheck prefix/suffix,\nfind a substring,\nsplit with a fixed delimiter"]
    Q --> Complex["Complex pattern matching,\nirregular formats,\nmany variations"]

    Simple --> NoRegex["Use the strings package\nFASTER and CLEARER"]
    Complex --> Regex["Consider Regex\n(but measure first)"]

    NoRegex --> NS1["strings.HasPrefix / HasSuffix"]
    NoRegex --> NS2["strings.Contains / Index"]
    NoRegex --> NS3["strings.Split / Fields"]
    NoRegex --> NS4["strings.TrimSpace / Trim"]

    Regex --> RA1["Format validation: email, URL, dates"]
    Regex --> RA2["Pattern extraction from free text"]
    Regex --> RA3["Complex pattern replacement"]

    style NoRegex fill:#e8f5e9
    style Regex fill:#e3f2fd
// Performance comparison for simple operations

// SLOW: regex for simple operations
rePrefix := regexp.MustCompile(`^http://`)
rePrefix.MatchString(url) // compilation + matching engine overhead

// FAST: the strings package for simple operations
strings.HasPrefix(url, "http://") // direct, no overhead

// SLOW: regex for splitting with a fixed delimiter
reComma := regexp.MustCompile(`,`)
reComma.Split(csv, -1)

// FAST: strings.Split
strings.Split(csv, ",")

// Typical benchmarks:
// strings.HasPrefix: ~5 ns/op
// regexp.MatchString: ~150 ns/op (30x slower for simple cases)
// For complex patterns, the difference isn't significant because regex is genuinely needed

// When regex IS genuinely needed:
// - Highly variable formats (dates in various formats)
// - Complex validation (email, URLs with many edge cases)
// - Extraction from unstructured text
// - Replacement with complex conditions

Security: Regex from User Input #

// DANGEROUS: MustCompile with user input
func searchWithPattern(text, userPattern string) []string {
    re := regexp.MustCompile(userPattern) // PANICS if the pattern is invalid!
    return re.FindAllString(text, -1)
}

// SAFE: Compile with error handling
func searchWithPatternSafe(text, userPattern string) ([]string, error) {
    // Limit the pattern length to prevent ReDoS (doesn't apply to RE2,
    // but limiting complexity is still good practice)
    if len(userPattern) > 1000 {
        return nil, fmt.Errorf("regex pattern too long")
    }

    re, err := regexp.Compile(userPattern)
    if err != nil {
        return nil, fmt.Errorf("invalid regex pattern: %w", err)
    }

    return re.FindAllString(text, 100), nil // limit the result count too
}

// NOTE: Go uses RE2, which is NOT vulnerable to ReDoS
// (Catastrophic Backtracking) because it's guaranteed O(n).
// Unlike PCRE in PHP, Python, and Java, which can hang with certain patterns.
// Still, limit user-supplied patterns as good security practice.

Production Usage Patterns #

Input Sanitization Middleware #

var (
    reDangerousChars = regexp.MustCompile(`[<>'"&;]`)
    reMultipleSpaces = regexp.MustCompile(`\s{2,}`)
    reNonAlphanumeric = regexp.MustCompile(`[^a-zA-Z0-9\s]`)
)

func sanitizeInput(input string) string {
    // Remove dangerous characters for HTML
    clean := reDangerousChars.ReplaceAllString(input, "")
    // Normalize excessive spaces
    clean = reMultipleSpaces.ReplaceAllString(clean, " ")
    return strings.TrimSpace(clean)
}

func makeSlug(title string) string {
    // Convert to lowercase
    slug := strings.ToLower(title)
    // Replace non-alphanumeric characters with -
    slug = reNonAlphanumeric.ReplaceAllString(slug, "-")
    // Normalize multiple -
    reDash := regexp.MustCompile(`-+`)
    slug = reDash.ReplaceAllString(slug, "-")
    // Remove - at the start and end
    return strings.Trim(slug, "-")
}

// Usage
fmt.Println(makeSlug("Go Guide: Standard Library Package!"))
// go-guide-standard-library-package

A Log Parser with Named Groups #

// Nginx log format: 192.168.1.1 - - [15/Mar/2024:14:30:00 +0700] "GET /api/v1 HTTP/1.1" 200 1234
var reNginxLog = regexp.MustCompile(
    `(?P<ip>\d+\.\d+\.\d+\.\d+) - - ` +
    `\[(?P<time>[^\]]+)\] ` +
    `"(?P<method>\w+) (?P<path>[^ ]+) HTTP/[\d.]+" ` +
    `(?P<status>\d+) (?P<bytes>\d+)`)

type NginxLogEntry struct {
    IP     string
    Time   string
    Method string
    Path   string
    Status int
    Bytes  int
}

func parseNginxLog(line string) (*NginxLogEntry, error) {
    match := reNginxLog.FindStringSubmatch(line)
    if match == nil {
        return nil, fmt.Errorf("unrecognized log format: %q", line)
    }

    names := reNginxLog.SubexpNames()
    fields := make(map[string]string)
    for i, name := range names {
        if name != "" {
            fields[name] = match[i]
        }
    }

    status, _ := strconv.Atoi(fields["status"])
    bytes, _ := strconv.Atoi(fields["bytes"])

    return &NginxLogEntry{
        IP:     fields["ip"],
        Time:   fields["time"],
        Method: fields["method"],
        Path:   fields["path"],
        Status: status,
        Bytes:  bytes,
    }, nil
}

Redacting Sensitive Data #

var (
    reCreditCard2 = regexp.MustCompile(`\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b`)
    reNIK2        = regexp.MustCompile(`\b\d{16}\b`)
    reEmail3      = regexp.MustCompile(`[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}`)
    rePhone2      = regexp.MustCompile(`\b0\d{9,11}\b`)
)

func redactSensitiveData(text string) string {
    // Mask credit cards
    text = reCreditCard2.ReplaceAllStringFunc(text, func(m string) string {
        clean := regexp.MustCompile(`[\s-]`).ReplaceAllString(m, "")
        return "**** **** **** " + clean[len(clean)-4:]
    })

    // Mask NIKs
    text = reNIK2.ReplaceAllStringFunc(text, func(m string) string {
        return m[:6] + "**********"
    })

    // Mask emails
    text = reEmail3.ReplaceAllStringFunc(text, func(m string) string {
        parts := strings.Split(m, "@")
        if len(parts) != 2 {
            return m
        }
        name := parts[0]
        if len(name) > 2 {
            name = name[:2] + strings.Repeat("*", len(name)-2)
        }
        return name + "@" + parts[1]
    })

    return text
}

// Usage
log := "NIK: 3273011234567890, Email: [email protected], Card: 4111 1111 1111 1111"
fmt.Println(redactSensitiveData(log))
// NIK: 327301**********, Email: bu*************@gmail.com, Card: **** **** **** 1111

When to Switch to Alternatives #

Keep using regexp if:
  ✓ Validating complex formats: email, URLs, dates in various formats
  ✓ Extracting patterns from unstructured text: logs, HTML, documents
  ✓ Complex pattern replacement with dynamic conditions
  ✓ Parsing formats with many variations

Use the strings package if:
  ✗ Finding a fixed substring → strings.Contains, strings.Index
  ✗ Splitting with a fixed delimiter → strings.Split
  ✗ Checking prefixes/suffixes → strings.HasPrefix, strings.HasSuffix
  ✗ Removing whitespace → strings.TrimSpace
  ✗ Replacing a fixed string → strings.ReplaceAll
  (all of these are 10-100x faster than the regex equivalents)

Use strconv if:
  ✗ Validating numbers → strconv.Atoi, strconv.ParseFloat
  ✗ Parsing numbers from strings → more precise and clear

Consider dedicated parsers if:
  ✗ Parsing HTML/XML → golang.org/x/net/html or encoding/xml
  ✗ Parsing JSON → encoding/json
  ✗ Parsing YAML → gopkg.in/yaml.v3
  ✗ Parsing very complex formats → a structured parser is more maintainable

Summary #

  • Compile regexes once as package variables with MustCompile — don’t compile inside frequently called functions; the overhead is significant.
  • MustCompile only for constant patterns you wrote yourself — for patterns from user input, always use Compile and handle the error.
  • Go uses RE2, not PCRE — no lookahead/lookbehind, no backreferences, but guaranteed safe from ReDoS because of its O(n) complexity.
  • Named capturing groups with (?P<name>...) and SubexpIndex("name") make code much more readable than accessing match[1], match[2] numerically.
  • ReplaceAllStringFunc for dynamic replacement — use it when the replacement value depends on the match content, like number transformations or data masking.
  • regexp.QuoteMeta(s) to use a literal string as a pattern — important when the pattern contains special regex characters from user input.
  • Measure before using regex — for simple operations like prefix checks or fixed-delimiter splits, the strings package is 10-100x faster.
  • FindAllStringSubmatch returns a slice of slices — result[i][0] is the i-th match, result[i][1] is the first group of the i-th match.
  • Regex compilation isn’t safe for concurrent use — but executing an already-compiled *regexp.Regexp is safe from many goroutines at once.

← Previous: Filepath   Next: Encoding Csv →

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