Static patterns versus dynamic input
You are writing a service that validates user input. You have a strict email format hardcoded in your code. You also added a feature where admins can define custom filters for search results. The email regex is safe; it lives in your source code and never changes. The admin filters come from a database or a config file. A typo in the email regex should stop the build. A typo in an admin filter should return a friendly error message, not crash the server.
Go gives you two functions for compiling regular expressions. One panics on error. The other returns an error. Picking the wrong one turns a harmless typo into a production outage, or forces you to write boilerplate error handling for code that can never fail.
How compilation works
Compiling a regular expression means parsing the pattern string and turning it into an internal instruction set the engine can execute efficiently. This process checks for syntax errors, resolves character classes, and builds a state machine. It takes CPU cycles.
Think of regexp.Compile as a baker. You hand it a recipe. The baker checks the ingredients and steps. If the recipe makes sense, the baker hands you the bread. If the recipe is nonsense, the baker tells you exactly what went wrong.
regexp.MustCompile is an automated factory line. It assumes the recipe is perfect. It bakes the bread instantly. If the recipe has a mistake, the factory line grinds to a halt and blows a fuse. The program panics.
The result of compilation is a *regexp.Regexp object. This object is immutable. Once created, the internal state machine never changes. Methods like MatchString and FindAll allocate temporary buffers for execution. These buffers are local to the call. You can pass the same *regexp.Regexp to a thousand goroutines. They will not interfere with each other.
Immutable regexes are thread-safe. Share them freely.
Minimal example
Here is the difference in action. Compile returns a value and an error. MustCompile returns just the value and crashes if the pattern is bad.
package main
import (
"fmt"
"regexp"
)
// Compile parses the pattern and returns the compiled regex plus an error.
// Check the error to handle invalid syntax gracefully.
func safeCompile() {
re, err := regexp.Compile(`[a-z]+`)
if err != nil {
fmt.Println("Bad regex:", err)
return
}
fmt.Println("Match:", re.MatchString("hello"))
}
// MustCompile wraps Compile and panics on error.
// Safe for static patterns verified at development time.
func mustCompile() {
re := regexp.MustCompile(`[a-z]+`)
fmt.Println("Match:", re.MatchString("hello"))
}
func main() {
safeCompile()
mustCompile()
}
Under the hood
regexp.MustCompile is a thin wrapper around regexp.Compile. The source code calls Compile, checks the error, and panics if one exists. The heavy lifting is identical. Both functions parse the pattern and build the state machine.
The difference is purely in error handling strategy. MustCompile treats a syntax error as a programming bug. It assumes the pattern is part of your code and should have been caught during testing. Compile treats a syntax error as a runtime condition. It assumes the pattern might come from an external source and could be invalid.
If you pass a bad pattern to MustCompile, the program crashes with panic: regexp: Compile(...): error parsing regexp: missing closing ]: [...]. The stack trace points to the line where MustCompile was called. This is the desired behavior for static patterns. A panic prevents your service from starting with broken validation logic.
If you ignore the error returned by Compile, you end up with a nil *regexp.Regexp. Calling re.MatchString on a nil pointer causes a runtime panic. The compiler won't catch this. The error message looks like panic: runtime error: invalid memory address or nil pointer dereference. The fix is simple: always handle the error immediately after Compile.
The Go community embraces the if err != nil boilerplate. It forces you to acknowledge that compilation can fail. Don't try to hide it.
Realistic usage
In a real application, you often mix static validation rules with dynamic user filters. Here is a handler that validates a hardcoded email format and also supports a user-provided search pattern.
package main
import (
"encoding/json"
"regexp"
)
// emailRegex is compiled once when the package initializes.
// MustCompile panics if the pattern is invalid, failing the build.
// This is correct for static patterns: a typo here is a developer error.
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
// SearchRequest represents the JSON payload from the client.
// The Pattern field contains user-provided regex syntax.
type SearchRequest struct {
Pattern string `json:"pattern"`
}
// SearchHandler validates the request and compiles the user pattern.
// It uses Compile to return an HTTP error on invalid syntax.
func SearchHandler(w http.ResponseWriter, r *http.Request) {
var req SearchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad JSON", http.StatusBadRequest)
return
}
// Compile handles dynamic patterns from untrusted input.
// If the syntax is bad, err is non-nil and the program continues.
re, err := regexp.Compile(req.Pattern)
if err != nil {
http.Error(w, "Invalid regex pattern", http.StatusBadRequest)
return
}
// The compiled regex is safe to use.
// regexp.Regexp is immutable and thread-safe.
results := filterItems(re)
json.NewEncoder(w).Encode(results)
}
Static patterns panic on code errors. Dynamic patterns return errors to the user.
Caching dynamic patterns
When patterns are dynamic but repeat often, you need a cache. Compilation is expensive. If you compile the same pattern thousands of times per second, you waste CPU cycles. Store the *regexp.Regexp in a map. The compiled object is thread-safe. You can share it across all goroutines.
package main
import (
"regexp"
"sync"
)
// patternCache stores compiled regexes to avoid recompilation.
// sync.Map is optimized for high concurrency.
var patternCache = sync.Map{}
// getOrCompile returns a cached regex or compiles and stores a new one.
// It returns an error if the pattern syntax is invalid.
func getOrCompile(pattern string) (*regexp.Regexp, error) {
// Load checks the cache first.
if cached, ok := patternCache.Load(pattern); ok {
return cached.(*regexp.Regexp), nil
}
// Compile the pattern if not found.
re, err := regexp.Compile(pattern)
if err != nil {
return nil, err
}
// LoadOrStore handles race conditions on insertion.
actual, _ := patternCache.LoadOrStore(pattern, re)
return actual.(*regexp.Regexp), nil
}
Cache dynamic patterns. Handle errors immediately. Never trust user syntax.
Pitfalls and errors
The biggest mistake is using MustCompile with data that comes from outside your code. If a user sends a malformed pattern, MustCompile panics. The panic stops the goroutine. If you are in an HTTP handler, the server recovers and returns a 500 error. If you are in a background worker, the worker dies. A malicious user can crash your service by sending invalid regex syntax. Always use Compile for dynamic input.
Another common trap is calling Compile inside a tight loop. Compilation parses the pattern and builds a state machine. If you compile the same pattern repeatedly, you waste CPU cycles. Cache the result. Store the *regexp.Regexp in a variable or a map.
If you forget to check the error from Compile, you get a nil pointer panic later. The compiler rejects programs with unused variables, but it won't warn you if you assign the error to _ and proceed. The runtime panic is panic: runtime error: invalid memory address or nil pointer dereference. The stack trace points to the method call on the nil regex, not the compilation site. This makes debugging harder. Always check the error right after Compile.
The receiver name convention applies to methods on *regexp.Regexp. The standard library uses (re *Regexp) or similar short names. When you write helper functions, follow the same pattern. Keep receiver names short and descriptive.
Trust gofmt. Argue logic, not formatting.
When to use which
Use regexp.MustCompile when the pattern is hardcoded in your source code and verified during development.
Use regexp.MustCompile for package-level variables that initialize once at startup.
Use regexp.Compile when the pattern comes from user input, configuration files, or database records.
Use regexp.Compile when you need to return an error to the caller instead of crashing the program.
Use a cached *regexp.Regexp when you reuse the same pattern multiple times to avoid recompilation overhead.
Use regexp.MustCompile for test fixtures where a bad pattern indicates a broken test setup.
Panic on code errors. Handle input errors. Cache the result.