Skip to Content
Course content

144: Compiling and Matching Regex in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this mistake in countless pull requests, even from developers who have been using Go for a while. They treat the regexp package like a set of quick utility functions, calling regexp.MatchString every time they need to check a value. It looks clean and concise, but it's a hidden performance landmine.

The "MatchString is a shortcut" Trap

When you call regexp.MatchString("^[a-z]+$", input), you aren't just matching a string. Under the hood, Go has to parse that regular expression, compile it into a finite automaton, and then execute it against your input. If you do this inside a loop—say, validating 10,000 email addresses in a CSV import—you are paying the compilation tax 10,000 times for the exact same pattern.

// DON'T DO THIS in a loop
for _, email := range emails {
    // This recompiles the regex every single iteration!
    if matched, _ := regexp.MatchString(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`, email); matched {
        fmt.Println("Valid email:", email)
    }
}

It works, sure. But your profiler will eventually show that a massive chunk of your CPU time is spent in regexp.compile rather than actually processing data. It's a classic case of "convenience over correctness."

Pre-compiling for Production Performance

The right way to handle this is to compile your regex once and reuse the resulting *regexp.Regexp object. In most cases, you'll want to do this at the package level using regexp.MustCompile. I prefer MustCompile over Compile for global variables because if your regex is syntactically wrong, the program should panic immediately on startup rather than failing silently later in the execution flow.

var emailRegex = regexp.MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`)

func ValidateEmails(emails []string) {
    for _, email := range emails {
        // Now we are just executing the pre-compiled state machine
        if emailRegex.MatchString(email) {
            fmt.Println("Valid email:", email)
        }
    }
}

By moving the compilation outside the loop, you've turned a heavy operation into a lightweight one. Now, the regex is compiled once when the program starts, and each call to MatchString simply traverses the already-built automaton.

Capturing Data with Submatches

Matching is great for booleans, but usually, we need the data inside the match. This is where FindStringSubmatch comes in. It returns a slice of strings: the first element is the full match, and every subsequent element corresponds to a capturing group (the parts in parentheses).

Let's say we're parsing a custom Order ID like ORD-12345-USA where we need the numeric ID and the country code.

var orderRegex = regexp.MustCompile(`^ORD-(\d+)-([A-Z]{3})$`)

func ParseOrder(id string) {
    matches := orderRegex.FindStringSubmatch(id)
    if len(matches) < 3 {
        fmt.Println("Invalid order format")
        return
    }

    // matches[0] is "ORD-12345-USA"
    orderNum := matches[1] // "12345"
    country := matches[2]  // "USA"
    fmt.Printf("Order #%s from %s\n", orderNum, country)
}

One quick tip: always check the length of the resulting slice before accessing indices. If the regex doesn't match, FindStringSubmatch returns nil. Trying to access matches[1] on a nil slice is a fast track to a runtime panic.




📋 Practical Task

Extracting Log Levels and Messages from System Logs

You are building a log analyzer. Your task is to create a program that processes a slice of raw log strings and extracts the log level (e.g., INFO, ERROR, WARN) and the message.

The logs follow this specific format: [LEVEL] Message text here

Requirements:

  • Define a package-level regular expression using regexp.MustCompile to capture the level and the message.
  • Create a function ParseLog(log string) (string, string, bool) that returns the level, the message, and a boolean indicating if the match was successful.
  • Ensure your regex specifically captures the text inside the brackets [] and everything following the closing bracket and the subsequent space.
package main

import (
    "fmt"
    "regexp"
)

// TODO: Define your compiled regex here

func ParseLog(log string) (string, string, bool) {
    // TODO: Implement the matching logic using FindStringSubmatch
    return "", "", false
}

func main() {
    logs := []string{
        "[INFO] Server started on port 8080",
        "[ERROR] Database connection failed",
        "Invalid log line",
        "[WARN] Disk space reaching 90%",
    }

    for _, l := range logs {
        level, msg, ok := ParseLog(l)
        if ok {
            fmt.Printf("Level: %s | Msg: %s\n", level, msg)
        } else {
            fmt.Printf("Skipping malformed log: %s\n", l)
        }
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.